changeset 1148:c0d841cb8ab9 tony-2.0-integration

Merge latest SV 3.0 branch code
author Chris Cannam
date Fri, 19 Aug 2016 15:58:57 +0100
parents 96cf499fad62 (current diff) 1badacff7ab2 (diff)
children 0d04b1cdb9f9
files view/LayerGeometryProvider.h
diffstat 52 files changed, 4704 insertions(+), 3676 deletions(-) [+]
line wrap: on
line diff
--- a/acinclude.m4	Tue Oct 20 12:55:09 2015 +0100
+++ b/acinclude.m4	Fri Aug 19 15:58:57 2016 +0100
@@ -69,6 +69,9 @@
    	AC_CHECK_PROG(QMAKE, qmake-qt5, $QTDIR/bin/qmake-qt5,,$QTDIR/bin/)
 fi
 if test x$QMAKE = x ; then
+   	AC_CHECK_PROG(QMAKE, qt5-qmake, $QTDIR/bin/qt5-qmake,,$QTDIR/bin/)
+fi
+if test x$QMAKE = x ; then
    	AC_CHECK_PROG(QMAKE, qmake, $QTDIR/bin/qmake,,$QTDIR/bin/)
 fi
 if test x$QMAKE = x ; then
@@ -78,6 +81,9 @@
    	AC_CHECK_PROG(QMAKE, qmake-qt5, qmake-qt5,,$PATH)
 fi
 if test x$QMAKE = x ; then
+   	AC_CHECK_PROG(QMAKE, qt5-qmake, qt5-qmake,,$PATH)
+fi
+if test x$QMAKE = x ; then
    	AC_CHECK_PROG(QMAKE, qmake, qmake,,$PATH)
 fi
 if test x$QMAKE = x ; then
--- a/configure	Tue Oct 20 12:55:09 2015 +0100
+++ b/configure	Fri Aug 19 15:58:57 2016 +0100
@@ -646,16 +646,12 @@
 libpulse_CFLAGS
 JACK_LIBS
 JACK_CFLAGS
-portaudio_2_0_LIBS
-portaudio_2_0_CFLAGS
+portaudio_LIBS
+portaudio_CFLAGS
 liblo_LIBS
 liblo_CFLAGS
 rubberband_LIBS
 rubberband_CFLAGS
-vamphostsdk_LIBS
-vamphostsdk_CFLAGS
-vamp_LIBS
-vamp_CFLAGS
 samplerate_LIBS
 samplerate_CFLAGS
 sndfile_LIBS
@@ -756,16 +752,12 @@
 sndfile_LIBS
 samplerate_CFLAGS
 samplerate_LIBS
-vamp_CFLAGS
-vamp_LIBS
-vamphostsdk_CFLAGS
-vamphostsdk_LIBS
 rubberband_CFLAGS
 rubberband_LIBS
 liblo_CFLAGS
 liblo_LIBS
-portaudio_2_0_CFLAGS
-portaudio_2_0_LIBS
+portaudio_CFLAGS
+portaudio_LIBS
 JACK_CFLAGS
 JACK_LIBS
 libpulse_CFLAGS
@@ -1423,12 +1415,6 @@
               C compiler flags for samplerate, overriding pkg-config
   samplerate_LIBS
               linker flags for samplerate, overriding pkg-config
-  vamp_CFLAGS C compiler flags for vamp, overriding pkg-config
-  vamp_LIBS   linker flags for vamp, overriding pkg-config
-  vamphostsdk_CFLAGS
-              C compiler flags for vamphostsdk, overriding pkg-config
-  vamphostsdk_LIBS
-              linker flags for vamphostsdk, overriding pkg-config
   rubberband_CFLAGS
               C compiler flags for rubberband, overriding pkg-config
   rubberband_LIBS
@@ -1436,10 +1422,10 @@
   liblo_CFLAGS
               C compiler flags for liblo, overriding pkg-config
   liblo_LIBS  linker flags for liblo, overriding pkg-config
-  portaudio_2_0_CFLAGS
-              C compiler flags for portaudio_2_0, overriding pkg-config
-  portaudio_2_0_LIBS
-              linker flags for portaudio_2_0, overriding pkg-config
+  portaudio_CFLAGS
+              C compiler flags for portaudio, overriding pkg-config
+  portaudio_LIBS
+              linker flags for portaudio, overriding pkg-config
   JACK_CFLAGS C compiler flags for JACK, overriding pkg-config
   JACK_LIBS   linker flags for JACK, overriding pkg-config
   libpulse_CFLAGS
@@ -4137,6 +4123,45 @@
 
 fi
 if test x$QMAKE = x ; then
+   	# Extract the first word of "qt5-qmake", so it can be a program name with args.
+set dummy qt5-qmake; ac_word=$2
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+$as_echo_n "checking for $ac_word... " >&6; }
+if ${ac_cv_prog_QMAKE+:} false; then :
+  $as_echo_n "(cached) " >&6
+else
+  if test -n "$QMAKE"; then
+  ac_cv_prog_QMAKE="$QMAKE" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $QTDIR/bin/
+do
+  IFS=$as_save_IFS
+  test -z "$as_dir" && as_dir=.
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then
+    ac_cv_prog_QMAKE="$QTDIR/bin/qt5-qmake"
+    $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+QMAKE=$ac_cv_prog_QMAKE
+if test -n "$QMAKE"; then
+  { $as_echo "$as_me:${as_lineno-$LINENO}: result: $QMAKE" >&5
+$as_echo "$QMAKE" >&6; }
+else
+  { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
+$as_echo "no" >&6; }
+fi
+
+
+fi
+if test x$QMAKE = x ; then
    	# Extract the first word of "qmake", so it can be a program name with args.
 set dummy qmake; ac_word=$2
 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
@@ -4254,6 +4279,45 @@
 
 fi
 if test x$QMAKE = x ; then
+   	# Extract the first word of "qt5-qmake", so it can be a program name with args.
+set dummy qt5-qmake; ac_word=$2
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+$as_echo_n "checking for $ac_word... " >&6; }
+if ${ac_cv_prog_QMAKE+:} false; then :
+  $as_echo_n "(cached) " >&6
+else
+  if test -n "$QMAKE"; then
+  ac_cv_prog_QMAKE="$QMAKE" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+  IFS=$as_save_IFS
+  test -z "$as_dir" && as_dir=.
+    for ac_exec_ext in '' $ac_executable_extensions; do
+  if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then
+    ac_cv_prog_QMAKE="qt5-qmake"
+    $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5
+    break 2
+  fi
+done
+  done
+IFS=$as_save_IFS
+
+fi
+fi
+QMAKE=$ac_cv_prog_QMAKE
+if test -n "$QMAKE"; then
+  { $as_echo "$as_me:${as_lineno-$LINENO}: result: $QMAKE" >&5
+$as_echo "$QMAKE" >&6; }
+else
+  { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
+$as_echo "no" >&6; }
+fi
+
+
+fi
+if test x$QMAKE = x ; then
    	# Extract the first word of "qmake", so it can be a program name with args.
 set dummy qmake; ac_word=$2
 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
@@ -5148,18 +5212,18 @@
 fi
 
 
-SV_MODULE_MODULE=vamp
-SV_MODULE_VERSION_TEST="vamp >= 2.1"
-SV_MODULE_HEADER=vamp/vamp.h
-SV_MODULE_LIB=
-SV_MODULE_FUNC=
-SV_MODULE_HAVE=HAVE_$(echo vamp | tr 'a-z' 'A-Z')
+SV_MODULE_MODULE=rubberband
+SV_MODULE_VERSION_TEST="rubberband"
+SV_MODULE_HEADER=rubberband/RubberBandStretcher.h
+SV_MODULE_LIB=rubberband
+SV_MODULE_FUNC=rubberband_new
+SV_MODULE_HAVE=HAVE_$(echo rubberband | tr 'a-z' 'A-Z')
 SV_MODULE_FAILED=1
-if test -n "$vamp_LIBS" ; then
+if test -n "$rubberband_LIBS" ; then
    { $as_echo "$as_me:${as_lineno-$LINENO}: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&5
 $as_echo "$as_me: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&6;}
-   CXXFLAGS="$CXXFLAGS $vamp_CFLAGS"
-   LIBS="$LIBS $vamp_LIBS"
+   CXXFLAGS="$CXXFLAGS $rubberband_CFLAGS"
+   LIBS="$LIBS $rubberband_LIBS"
    SV_MODULE_FAILED=""
 fi
 if test -z "$SV_MODULE_VERSION_TEST" ; then
@@ -5168,11 +5232,11 @@
 if test -n "$SV_MODULE_FAILED" && test -n "$PKG_CONFIG"; then
 
 pkg_failed=no
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for vamp" >&5
-$as_echo_n "checking for vamp... " >&6; }
-
-if test -n "$vamp_CFLAGS"; then
-    pkg_cv_vamp_CFLAGS="$vamp_CFLAGS"
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for rubberband" >&5
+$as_echo_n "checking for rubberband... " >&6; }
+
+if test -n "$rubberband_CFLAGS"; then
+    pkg_cv_rubberband_CFLAGS="$rubberband_CFLAGS"
  elif test -n "$PKG_CONFIG"; then
     if test -n "$PKG_CONFIG" && \
     { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
@@ -5180,7 +5244,7 @@
   ac_status=$?
   $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
   test $ac_status = 0; }; then
-  pkg_cv_vamp_CFLAGS=`$PKG_CONFIG --cflags "$SV_MODULE_VERSION_TEST" 2>/dev/null`
+  pkg_cv_rubberband_CFLAGS=`$PKG_CONFIG --cflags "$SV_MODULE_VERSION_TEST" 2>/dev/null`
 		      test "x$?" != "x0" && pkg_failed=yes
 else
   pkg_failed=yes
@@ -5188,8 +5252,8 @@
  else
     pkg_failed=untried
 fi
-if test -n "$vamp_LIBS"; then
-    pkg_cv_vamp_LIBS="$vamp_LIBS"
+if test -n "$rubberband_LIBS"; then
+    pkg_cv_rubberband_LIBS="$rubberband_LIBS"
  elif test -n "$PKG_CONFIG"; then
     if test -n "$PKG_CONFIG" && \
     { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
@@ -5197,7 +5261,7 @@
   ac_status=$?
   $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
   test $ac_status = 0; }; then
-  pkg_cv_vamp_LIBS=`$PKG_CONFIG --libs "$SV_MODULE_VERSION_TEST" 2>/dev/null`
+  pkg_cv_rubberband_LIBS=`$PKG_CONFIG --libs "$SV_MODULE_VERSION_TEST" 2>/dev/null`
 		      test "x$?" != "x0" && pkg_failed=yes
 else
   pkg_failed=yes
@@ -5218,12 +5282,12 @@
         _pkg_short_errors_supported=no
 fi
         if test $_pkg_short_errors_supported = yes; then
-	        vamp_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
+	        rubberband_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
         else
-	        vamp_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
+	        rubberband_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
         fi
 	# Put the nasty error message in config.log where it belongs
-	echo "$vamp_PKG_ERRORS" >&5
+	echo "$rubberband_PKG_ERRORS" >&5
 
 	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
 $as_echo "$as_me: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
@@ -5233,11 +5297,11 @@
 	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
 $as_echo "$as_me: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
 else
-	vamp_CFLAGS=$pkg_cv_vamp_CFLAGS
-	vamp_LIBS=$pkg_cv_vamp_LIBS
+	rubberband_CFLAGS=$pkg_cv_rubberband_CFLAGS
+	rubberband_LIBS=$pkg_cv_rubberband_LIBS
         { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
 $as_echo "yes" >&6; }
-	HAVES="$HAVES $SV_MODULE_HAVE";CXXFLAGS="$CXXFLAGS $vamp_CFLAGS";LIBS="$LIBS $vamp_LIBS";SV_MODULE_FAILED=""
+	HAVES="$HAVES $SV_MODULE_HAVE";CXXFLAGS="$CXXFLAGS $rubberband_CFLAGS";LIBS="$LIBS $rubberband_LIBS";SV_MODULE_FAILED=""
 fi
 fi
 if test -n "$SV_MODULE_FAILED"; then
@@ -5299,18 +5363,19 @@
 fi
 
 
-SV_MODULE_MODULE=vamphostsdk
-SV_MODULE_VERSION_TEST="vamp-hostsdk >= 2.5"
-SV_MODULE_HEADER=vamp-hostsdk/PluginLoader.h
-SV_MODULE_LIB=vamp-hostsdk
-SV_MODULE_FUNC=libvamphostsdk_v_2_5_present
-SV_MODULE_HAVE=HAVE_$(echo vamphostsdk | tr 'a-z' 'A-Z')
+
+SV_MODULE_MODULE=liblo
+SV_MODULE_VERSION_TEST=""
+SV_MODULE_HEADER=lo/lo.h
+SV_MODULE_LIB=lo
+SV_MODULE_FUNC=lo_address_new
+SV_MODULE_HAVE=HAVE_$(echo liblo | tr 'a-z' 'A-Z')
 SV_MODULE_FAILED=1
-if test -n "$vamphostsdk_LIBS" ; then
+if test -n "$liblo_LIBS" ; then
    { $as_echo "$as_me:${as_lineno-$LINENO}: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&5
 $as_echo "$as_me: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&6;}
-   CXXFLAGS="$CXXFLAGS $vamphostsdk_CFLAGS"
-   LIBS="$LIBS $vamphostsdk_LIBS"
+   CXXFLAGS="$CXXFLAGS $liblo_CFLAGS"
+   LIBS="$LIBS $liblo_LIBS"
    SV_MODULE_FAILED=""
 fi
 if test -z "$SV_MODULE_VERSION_TEST" ; then
@@ -5319,11 +5384,11 @@
 if test -n "$SV_MODULE_FAILED" && test -n "$PKG_CONFIG"; then
 
 pkg_failed=no
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for vamphostsdk" >&5
-$as_echo_n "checking for vamphostsdk... " >&6; }
-
-if test -n "$vamphostsdk_CFLAGS"; then
-    pkg_cv_vamphostsdk_CFLAGS="$vamphostsdk_CFLAGS"
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for liblo" >&5
+$as_echo_n "checking for liblo... " >&6; }
+
+if test -n "$liblo_CFLAGS"; then
+    pkg_cv_liblo_CFLAGS="$liblo_CFLAGS"
  elif test -n "$PKG_CONFIG"; then
     if test -n "$PKG_CONFIG" && \
     { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
@@ -5331,7 +5396,7 @@
   ac_status=$?
   $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
   test $ac_status = 0; }; then
-  pkg_cv_vamphostsdk_CFLAGS=`$PKG_CONFIG --cflags "$SV_MODULE_VERSION_TEST" 2>/dev/null`
+  pkg_cv_liblo_CFLAGS=`$PKG_CONFIG --cflags "$SV_MODULE_VERSION_TEST" 2>/dev/null`
 		      test "x$?" != "x0" && pkg_failed=yes
 else
   pkg_failed=yes
@@ -5339,8 +5404,8 @@
  else
     pkg_failed=untried
 fi
-if test -n "$vamphostsdk_LIBS"; then
-    pkg_cv_vamphostsdk_LIBS="$vamphostsdk_LIBS"
+if test -n "$liblo_LIBS"; then
+    pkg_cv_liblo_LIBS="$liblo_LIBS"
  elif test -n "$PKG_CONFIG"; then
     if test -n "$PKG_CONFIG" && \
     { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
@@ -5348,7 +5413,7 @@
   ac_status=$?
   $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
   test $ac_status = 0; }; then
-  pkg_cv_vamphostsdk_LIBS=`$PKG_CONFIG --libs "$SV_MODULE_VERSION_TEST" 2>/dev/null`
+  pkg_cv_liblo_LIBS=`$PKG_CONFIG --libs "$SV_MODULE_VERSION_TEST" 2>/dev/null`
 		      test "x$?" != "x0" && pkg_failed=yes
 else
   pkg_failed=yes
@@ -5369,40 +5434,42 @@
         _pkg_short_errors_supported=no
 fi
         if test $_pkg_short_errors_supported = yes; then
-	        vamphostsdk_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
+	        liblo_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
         else
-	        vamphostsdk_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
+	        liblo_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
         fi
 	# Put the nasty error message in config.log where it belongs
-	echo "$vamphostsdk_PKG_ERRORS" >&5
-
-	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
-$as_echo "$as_me: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
+	echo "$liblo_PKG_ERRORS" >&5
+
+	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
+$as_echo "$as_me: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
 elif test $pkg_failed = untried; then
      	{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
 $as_echo "no" >&6; }
-	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
-$as_echo "$as_me: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
-else
-	vamphostsdk_CFLAGS=$pkg_cv_vamphostsdk_CFLAGS
-	vamphostsdk_LIBS=$pkg_cv_vamphostsdk_LIBS
+	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
+$as_echo "$as_me: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
+else
+	liblo_CFLAGS=$pkg_cv_liblo_CFLAGS
+	liblo_LIBS=$pkg_cv_liblo_LIBS
         { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
 $as_echo "yes" >&6; }
-	HAVES="$HAVES $SV_MODULE_HAVE";CXXFLAGS="$CXXFLAGS $vamphostsdk_CFLAGS";LIBS="$LIBS $vamphostsdk_LIBS";SV_MODULE_FAILED=""
+	HAVES="$HAVES $SV_MODULE_HAVE";CXXFLAGS="$CXXFLAGS $liblo_CFLAGS";LIBS="$LIBS $liblo_LIBS";SV_MODULE_FAILED=""
 fi
 fi
 if test -n "$SV_MODULE_FAILED"; then
    as_ac_Header=`$as_echo "ac_cv_header_$SV_MODULE_HEADER" | $as_tr_sh`
 ac_fn_cxx_check_header_mongrel "$LINENO" "$SV_MODULE_HEADER" "$as_ac_Header" "$ac_includes_default"
 if eval test \"x\$"$as_ac_Header"\" = x"yes"; then :
-  HAVES="$HAVES $SV_MODULE_HAVE"
-else
-  as_fn_error $? "Failed to find header $SV_MODULE_HEADER for required module $SV_MODULE_MODULE" "$LINENO" 5
-fi
-
-
-   if test -n "$SV_MODULE_LIB"; then
-     as_ac_Lib=`$as_echo "ac_cv_lib_$SV_MODULE_LIB''_$SV_MODULE_FUNC" | $as_tr_sh`
+  HAVES="$HAVES $SV_MODULE_HAVE";SV_MODULE_FAILED=""
+else
+  { $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find header $SV_MODULE_HEADER for optional module $SV_MODULE_MODULE" >&5
+$as_echo "$as_me: Failed to find header $SV_MODULE_HEADER for optional module $SV_MODULE_MODULE" >&6;}
+fi
+
+
+   if test -z "$SV_MODULE_FAILED"; then
+      if test -n "$SV_MODULE_LIB"; then
+           as_ac_Lib=`$as_echo "ac_cv_lib_$SV_MODULE_LIB''_$SV_MODULE_FUNC" | $as_tr_sh`
 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $SV_MODULE_FUNC in -l$SV_MODULE_LIB" >&5
 $as_echo_n "checking for $SV_MODULE_FUNC in -l$SV_MODULE_LIB... " >&6; }
 if eval \${$as_ac_Lib+:} false; then :
@@ -5443,25 +5510,27 @@
 if eval test \"x\$"$as_ac_Lib"\" = x"yes"; then :
   LIBS="$LIBS -l$SV_MODULE_LIB"
 else
-  as_fn_error $? "Failed to find library $SV_MODULE_LIB for required module $SV_MODULE_MODULE" "$LINENO" 5
-fi
-
+  { $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find library $SV_MODULE_LIB for optional module $SV_MODULE_MODULE" >&5
+$as_echo "$as_me: Failed to find library $SV_MODULE_LIB for optional module $SV_MODULE_MODULE" >&6;}
+fi
+
+      fi
    fi
 fi
 
 
-SV_MODULE_MODULE=rubberband
-SV_MODULE_VERSION_TEST="rubberband"
-SV_MODULE_HEADER=rubberband/RubberBandStretcher.h
-SV_MODULE_LIB=rubberband
-SV_MODULE_FUNC=rubberband_new
-SV_MODULE_HAVE=HAVE_$(echo rubberband | tr 'a-z' 'A-Z')
+SV_MODULE_MODULE=portaudio
+SV_MODULE_VERSION_TEST="portaudio-2.0 >= 19"
+SV_MODULE_HEADER=portaudio.h
+SV_MODULE_LIB=portaudio
+SV_MODULE_FUNC=Pa_IsFormatSupported
+SV_MODULE_HAVE=HAVE_$(echo portaudio | tr 'a-z' 'A-Z')
 SV_MODULE_FAILED=1
-if test -n "$rubberband_LIBS" ; then
+if test -n "$portaudio_LIBS" ; then
    { $as_echo "$as_me:${as_lineno-$LINENO}: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&5
 $as_echo "$as_me: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&6;}
-   CXXFLAGS="$CXXFLAGS $rubberband_CFLAGS"
-   LIBS="$LIBS $rubberband_LIBS"
+   CXXFLAGS="$CXXFLAGS $portaudio_CFLAGS"
+   LIBS="$LIBS $portaudio_LIBS"
    SV_MODULE_FAILED=""
 fi
 if test -z "$SV_MODULE_VERSION_TEST" ; then
@@ -5470,11 +5539,11 @@
 if test -n "$SV_MODULE_FAILED" && test -n "$PKG_CONFIG"; then
 
 pkg_failed=no
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for rubberband" >&5
-$as_echo_n "checking for rubberband... " >&6; }
-
-if test -n "$rubberband_CFLAGS"; then
-    pkg_cv_rubberband_CFLAGS="$rubberband_CFLAGS"
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for portaudio" >&5
+$as_echo_n "checking for portaudio... " >&6; }
+
+if test -n "$portaudio_CFLAGS"; then
+    pkg_cv_portaudio_CFLAGS="$portaudio_CFLAGS"
  elif test -n "$PKG_CONFIG"; then
     if test -n "$PKG_CONFIG" && \
     { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
@@ -5482,7 +5551,7 @@
   ac_status=$?
   $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
   test $ac_status = 0; }; then
-  pkg_cv_rubberband_CFLAGS=`$PKG_CONFIG --cflags "$SV_MODULE_VERSION_TEST" 2>/dev/null`
+  pkg_cv_portaudio_CFLAGS=`$PKG_CONFIG --cflags "$SV_MODULE_VERSION_TEST" 2>/dev/null`
 		      test "x$?" != "x0" && pkg_failed=yes
 else
   pkg_failed=yes
@@ -5490,8 +5559,8 @@
  else
     pkg_failed=untried
 fi
-if test -n "$rubberband_LIBS"; then
-    pkg_cv_rubberband_LIBS="$rubberband_LIBS"
+if test -n "$portaudio_LIBS"; then
+    pkg_cv_portaudio_LIBS="$portaudio_LIBS"
  elif test -n "$PKG_CONFIG"; then
     if test -n "$PKG_CONFIG" && \
     { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
@@ -5499,7 +5568,7 @@
   ac_status=$?
   $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
   test $ac_status = 0; }; then
-  pkg_cv_rubberband_LIBS=`$PKG_CONFIG --libs "$SV_MODULE_VERSION_TEST" 2>/dev/null`
+  pkg_cv_portaudio_LIBS=`$PKG_CONFIG --libs "$SV_MODULE_VERSION_TEST" 2>/dev/null`
 		      test "x$?" != "x0" && pkg_failed=yes
 else
   pkg_failed=yes
@@ -5520,164 +5589,12 @@
         _pkg_short_errors_supported=no
 fi
         if test $_pkg_short_errors_supported = yes; then
-	        rubberband_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
+	        portaudio_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
         else
-	        rubberband_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
+	        portaudio_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
         fi
 	# Put the nasty error message in config.log where it belongs
-	echo "$rubberband_PKG_ERRORS" >&5
-
-	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
-$as_echo "$as_me: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
-elif test $pkg_failed = untried; then
-     	{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
-$as_echo "no" >&6; }
-	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
-$as_echo "$as_me: Failed to find required module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
-else
-	rubberband_CFLAGS=$pkg_cv_rubberband_CFLAGS
-	rubberband_LIBS=$pkg_cv_rubberband_LIBS
-        { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
-$as_echo "yes" >&6; }
-	HAVES="$HAVES $SV_MODULE_HAVE";CXXFLAGS="$CXXFLAGS $rubberband_CFLAGS";LIBS="$LIBS $rubberband_LIBS";SV_MODULE_FAILED=""
-fi
-fi
-if test -n "$SV_MODULE_FAILED"; then
-   as_ac_Header=`$as_echo "ac_cv_header_$SV_MODULE_HEADER" | $as_tr_sh`
-ac_fn_cxx_check_header_mongrel "$LINENO" "$SV_MODULE_HEADER" "$as_ac_Header" "$ac_includes_default"
-if eval test \"x\$"$as_ac_Header"\" = x"yes"; then :
-  HAVES="$HAVES $SV_MODULE_HAVE"
-else
-  as_fn_error $? "Failed to find header $SV_MODULE_HEADER for required module $SV_MODULE_MODULE" "$LINENO" 5
-fi
-
-
-   if test -n "$SV_MODULE_LIB"; then
-     as_ac_Lib=`$as_echo "ac_cv_lib_$SV_MODULE_LIB''_$SV_MODULE_FUNC" | $as_tr_sh`
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $SV_MODULE_FUNC in -l$SV_MODULE_LIB" >&5
-$as_echo_n "checking for $SV_MODULE_FUNC in -l$SV_MODULE_LIB... " >&6; }
-if eval \${$as_ac_Lib+:} false; then :
-  $as_echo_n "(cached) " >&6
-else
-  ac_check_lib_save_LIBS=$LIBS
-LIBS="-l$SV_MODULE_LIB  $LIBS"
-cat confdefs.h - <<_ACEOF >conftest.$ac_ext
-/* end confdefs.h.  */
-
-/* Override any GCC internal prototype to avoid an error.
-   Use char because int might match the return type of a GCC
-   builtin and then its argument prototype would still apply.  */
-#ifdef __cplusplus
-extern "C"
-#endif
-char $SV_MODULE_FUNC ();
-int
-main ()
-{
-return $SV_MODULE_FUNC ();
-  ;
-  return 0;
-}
-_ACEOF
-if ac_fn_cxx_try_link "$LINENO"; then :
-  eval "$as_ac_Lib=yes"
-else
-  eval "$as_ac_Lib=no"
-fi
-rm -f core conftest.err conftest.$ac_objext \
-    conftest$ac_exeext conftest.$ac_ext
-LIBS=$ac_check_lib_save_LIBS
-fi
-eval ac_res=\$$as_ac_Lib
-	       { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5
-$as_echo "$ac_res" >&6; }
-if eval test \"x\$"$as_ac_Lib"\" = x"yes"; then :
-  LIBS="$LIBS -l$SV_MODULE_LIB"
-else
-  as_fn_error $? "Failed to find library $SV_MODULE_LIB for required module $SV_MODULE_MODULE" "$LINENO" 5
-fi
-
-   fi
-fi
-
-
-
-SV_MODULE_MODULE=liblo
-SV_MODULE_VERSION_TEST=""
-SV_MODULE_HEADER=lo/lo.h
-SV_MODULE_LIB=lo
-SV_MODULE_FUNC=lo_address_new
-SV_MODULE_HAVE=HAVE_$(echo liblo | tr 'a-z' 'A-Z')
-SV_MODULE_FAILED=1
-if test -n "$liblo_LIBS" ; then
-   { $as_echo "$as_me:${as_lineno-$LINENO}: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&5
-$as_echo "$as_me: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&6;}
-   CXXFLAGS="$CXXFLAGS $liblo_CFLAGS"
-   LIBS="$LIBS $liblo_LIBS"
-   SV_MODULE_FAILED=""
-fi
-if test -z "$SV_MODULE_VERSION_TEST" ; then
-   SV_MODULE_VERSION_TEST=$SV_MODULE_MODULE
-fi
-if test -n "$SV_MODULE_FAILED" && test -n "$PKG_CONFIG"; then
-
-pkg_failed=no
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for liblo" >&5
-$as_echo_n "checking for liblo... " >&6; }
-
-if test -n "$liblo_CFLAGS"; then
-    pkg_cv_liblo_CFLAGS="$liblo_CFLAGS"
- elif test -n "$PKG_CONFIG"; then
-    if test -n "$PKG_CONFIG" && \
-    { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
-  ($PKG_CONFIG --exists --print-errors "$SV_MODULE_VERSION_TEST") 2>&5
-  ac_status=$?
-  $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
-  test $ac_status = 0; }; then
-  pkg_cv_liblo_CFLAGS=`$PKG_CONFIG --cflags "$SV_MODULE_VERSION_TEST" 2>/dev/null`
-		      test "x$?" != "x0" && pkg_failed=yes
-else
-  pkg_failed=yes
-fi
- else
-    pkg_failed=untried
-fi
-if test -n "$liblo_LIBS"; then
-    pkg_cv_liblo_LIBS="$liblo_LIBS"
- elif test -n "$PKG_CONFIG"; then
-    if test -n "$PKG_CONFIG" && \
-    { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
-  ($PKG_CONFIG --exists --print-errors "$SV_MODULE_VERSION_TEST") 2>&5
-  ac_status=$?
-  $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
-  test $ac_status = 0; }; then
-  pkg_cv_liblo_LIBS=`$PKG_CONFIG --libs "$SV_MODULE_VERSION_TEST" 2>/dev/null`
-		      test "x$?" != "x0" && pkg_failed=yes
-else
-  pkg_failed=yes
-fi
- else
-    pkg_failed=untried
-fi
-
-
-
-if test $pkg_failed = yes; then
-   	{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
-$as_echo "no" >&6; }
-
-if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
-        _pkg_short_errors_supported=yes
-else
-        _pkg_short_errors_supported=no
-fi
-        if test $_pkg_short_errors_supported = yes; then
-	        liblo_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
-        else
-	        liblo_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
-        fi
-	# Put the nasty error message in config.log where it belongs
-	echo "$liblo_PKG_ERRORS" >&5
+	echo "$portaudio_PKG_ERRORS" >&5
 
 	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
 $as_echo "$as_me: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
@@ -5687,166 +5604,11 @@
 	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
 $as_echo "$as_me: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
 else
-	liblo_CFLAGS=$pkg_cv_liblo_CFLAGS
-	liblo_LIBS=$pkg_cv_liblo_LIBS
+	portaudio_CFLAGS=$pkg_cv_portaudio_CFLAGS
+	portaudio_LIBS=$pkg_cv_portaudio_LIBS
         { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
 $as_echo "yes" >&6; }
-	HAVES="$HAVES $SV_MODULE_HAVE";CXXFLAGS="$CXXFLAGS $liblo_CFLAGS";LIBS="$LIBS $liblo_LIBS";SV_MODULE_FAILED=""
-fi
-fi
-if test -n "$SV_MODULE_FAILED"; then
-   as_ac_Header=`$as_echo "ac_cv_header_$SV_MODULE_HEADER" | $as_tr_sh`
-ac_fn_cxx_check_header_mongrel "$LINENO" "$SV_MODULE_HEADER" "$as_ac_Header" "$ac_includes_default"
-if eval test \"x\$"$as_ac_Header"\" = x"yes"; then :
-  HAVES="$HAVES $SV_MODULE_HAVE";SV_MODULE_FAILED=""
-else
-  { $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find header $SV_MODULE_HEADER for optional module $SV_MODULE_MODULE" >&5
-$as_echo "$as_me: Failed to find header $SV_MODULE_HEADER for optional module $SV_MODULE_MODULE" >&6;}
-fi
-
-
-   if test -z "$SV_MODULE_FAILED"; then
-      if test -n "$SV_MODULE_LIB"; then
-           as_ac_Lib=`$as_echo "ac_cv_lib_$SV_MODULE_LIB''_$SV_MODULE_FUNC" | $as_tr_sh`
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $SV_MODULE_FUNC in -l$SV_MODULE_LIB" >&5
-$as_echo_n "checking for $SV_MODULE_FUNC in -l$SV_MODULE_LIB... " >&6; }
-if eval \${$as_ac_Lib+:} false; then :
-  $as_echo_n "(cached) " >&6
-else
-  ac_check_lib_save_LIBS=$LIBS
-LIBS="-l$SV_MODULE_LIB  $LIBS"
-cat confdefs.h - <<_ACEOF >conftest.$ac_ext
-/* end confdefs.h.  */
-
-/* Override any GCC internal prototype to avoid an error.
-   Use char because int might match the return type of a GCC
-   builtin and then its argument prototype would still apply.  */
-#ifdef __cplusplus
-extern "C"
-#endif
-char $SV_MODULE_FUNC ();
-int
-main ()
-{
-return $SV_MODULE_FUNC ();
-  ;
-  return 0;
-}
-_ACEOF
-if ac_fn_cxx_try_link "$LINENO"; then :
-  eval "$as_ac_Lib=yes"
-else
-  eval "$as_ac_Lib=no"
-fi
-rm -f core conftest.err conftest.$ac_objext \
-    conftest$ac_exeext conftest.$ac_ext
-LIBS=$ac_check_lib_save_LIBS
-fi
-eval ac_res=\$$as_ac_Lib
-	       { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5
-$as_echo "$ac_res" >&6; }
-if eval test \"x\$"$as_ac_Lib"\" = x"yes"; then :
-  LIBS="$LIBS -l$SV_MODULE_LIB"
-else
-  { $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find library $SV_MODULE_LIB for optional module $SV_MODULE_MODULE" >&5
-$as_echo "$as_me: Failed to find library $SV_MODULE_LIB for optional module $SV_MODULE_MODULE" >&6;}
-fi
-
-      fi
-   fi
-fi
-
-
-SV_MODULE_MODULE=portaudio_2_0
-SV_MODULE_VERSION_TEST="portaudio-2.0 >= 19"
-SV_MODULE_HEADER=portaudio.h
-SV_MODULE_LIB=portaudio
-SV_MODULE_FUNC=Pa_IsFormatSupported
-SV_MODULE_HAVE=HAVE_$(echo portaudio_2_0 | tr 'a-z' 'A-Z')
-SV_MODULE_FAILED=1
-if test -n "$portaudio_2_0_LIBS" ; then
-   { $as_echo "$as_me:${as_lineno-$LINENO}: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&5
-$as_echo "$as_me: User set ${SV_MODULE_MODULE}_LIBS explicitly, skipping test for $SV_MODULE_MODULE" >&6;}
-   CXXFLAGS="$CXXFLAGS $portaudio_2_0_CFLAGS"
-   LIBS="$LIBS $portaudio_2_0_LIBS"
-   SV_MODULE_FAILED=""
-fi
-if test -z "$SV_MODULE_VERSION_TEST" ; then
-   SV_MODULE_VERSION_TEST=$SV_MODULE_MODULE
-fi
-if test -n "$SV_MODULE_FAILED" && test -n "$PKG_CONFIG"; then
-
-pkg_failed=no
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for portaudio_2_0" >&5
-$as_echo_n "checking for portaudio_2_0... " >&6; }
-
-if test -n "$portaudio_2_0_CFLAGS"; then
-    pkg_cv_portaudio_2_0_CFLAGS="$portaudio_2_0_CFLAGS"
- elif test -n "$PKG_CONFIG"; then
-    if test -n "$PKG_CONFIG" && \
-    { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
-  ($PKG_CONFIG --exists --print-errors "$SV_MODULE_VERSION_TEST") 2>&5
-  ac_status=$?
-  $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
-  test $ac_status = 0; }; then
-  pkg_cv_portaudio_2_0_CFLAGS=`$PKG_CONFIG --cflags "$SV_MODULE_VERSION_TEST" 2>/dev/null`
-		      test "x$?" != "x0" && pkg_failed=yes
-else
-  pkg_failed=yes
-fi
- else
-    pkg_failed=untried
-fi
-if test -n "$portaudio_2_0_LIBS"; then
-    pkg_cv_portaudio_2_0_LIBS="$portaudio_2_0_LIBS"
- elif test -n "$PKG_CONFIG"; then
-    if test -n "$PKG_CONFIG" && \
-    { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"\$SV_MODULE_VERSION_TEST\""; } >&5
-  ($PKG_CONFIG --exists --print-errors "$SV_MODULE_VERSION_TEST") 2>&5
-  ac_status=$?
-  $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
-  test $ac_status = 0; }; then
-  pkg_cv_portaudio_2_0_LIBS=`$PKG_CONFIG --libs "$SV_MODULE_VERSION_TEST" 2>/dev/null`
-		      test "x$?" != "x0" && pkg_failed=yes
-else
-  pkg_failed=yes
-fi
- else
-    pkg_failed=untried
-fi
-
-
-
-if test $pkg_failed = yes; then
-   	{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
-$as_echo "no" >&6; }
-
-if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
-        _pkg_short_errors_supported=yes
-else
-        _pkg_short_errors_supported=no
-fi
-        if test $_pkg_short_errors_supported = yes; then
-	        portaudio_2_0_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
-        else
-	        portaudio_2_0_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$SV_MODULE_VERSION_TEST" 2>&1`
-        fi
-	# Put the nasty error message in config.log where it belongs
-	echo "$portaudio_2_0_PKG_ERRORS" >&5
-
-	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
-$as_echo "$as_me: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
-elif test $pkg_failed = untried; then
-     	{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
-$as_echo "no" >&6; }
-	{ $as_echo "$as_me:${as_lineno-$LINENO}: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&5
-$as_echo "$as_me: Failed to find optional module $SV_MODULE_MODULE using pkg-config, trying again by old-fashioned means" >&6;}
-else
-	portaudio_2_0_CFLAGS=$pkg_cv_portaudio_2_0_CFLAGS
-	portaudio_2_0_LIBS=$pkg_cv_portaudio_2_0_LIBS
-        { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
-$as_echo "yes" >&6; }
-	HAVES="$HAVES $SV_MODULE_HAVE";CXXFLAGS="$CXXFLAGS $portaudio_2_0_CFLAGS";LIBS="$LIBS $portaudio_2_0_LIBS";SV_MODULE_FAILED=""
+	HAVES="$HAVES $SV_MODULE_HAVE";CXXFLAGS="$CXXFLAGS $portaudio_CFLAGS";LIBS="$LIBS $portaudio_LIBS";SV_MODULE_FAILED=""
 fi
 fi
 if test -n "$SV_MODULE_FAILED"; then
--- a/configure.ac	Tue Oct 20 12:55:09 2015 +0100
+++ b/configure.ac	Fri Aug 19 15:58:57 2016 +0100
@@ -83,8 +83,6 @@
 SV_MODULE_REQUIRED([fftw3f],[fftw3f >= 3.0.0],[fftw3.h],[fftw3f],[fftwf_execute])
 SV_MODULE_REQUIRED([sndfile],[sndfile >= 1.0.16],[sndfile.h],[sndfile],[sf_open])
 SV_MODULE_REQUIRED([samplerate],[samplerate >= 0.1.2],[samplerate.h],[samplerate],[src_new])
-SV_MODULE_REQUIRED([vamp],[vamp >= 2.1],[vamp/vamp.h],[],[])
-SV_MODULE_REQUIRED([vamphostsdk],[vamp-hostsdk >= 2.5],[vamp-hostsdk/PluginLoader.h],[vamp-hostsdk],[libvamphostsdk_v_2_5_present])
 SV_MODULE_REQUIRED([rubberband],[rubberband],[rubberband/RubberBandStretcher.h],[rubberband],[rubberband_new])
 
 SV_MODULE_OPTIONAL([liblo],[],[lo/lo.h],[lo],[lo_address_new])
--- a/layer/Colour3DPlotLayer.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/Colour3DPlotLayer.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -15,16 +15,23 @@
 
 #include "Colour3DPlotLayer.h"
 
-#include "view/View.h"
 #include "base/Profiler.h"
 #include "base/LogRange.h"
 #include "base/RangeMapper.h"
+
 #include "ColourMapper.h"
+#include "LayerGeometryProvider.h"
+#include "PaintAssistant.h"
+
+#include "data/model/Dense3DModelPeakCache.h"
+
+#include "view/ViewManager.h"
 
 #include <QPainter>
 #include <QImage>
 #include <QRect>
 #include <QTextStream>
+#include <QSettings>
 
 #include <iostream>
 
@@ -41,33 +48,92 @@
 
 Colour3DPlotLayer::Colour3DPlotLayer() :
     m_model(0),
-    m_cache(0),
-    m_peaksCache(0),
-    m_cacheValidStart(0),
-    m_cacheValidEnd(0),
-    m_colourScale(LinearScale),
+    m_colourScale(ColourScaleType::Linear),
     m_colourScaleSet(false),
     m_colourMap(0),
     m_gain(1.0),
-    m_binScale(LinearBinScale),
-    m_normalizeColumns(false),
+    m_binScale(BinScale::Linear),
+    m_normalization(ColumnNormalization::None),
     m_normalizeVisibleArea(false),
-    m_normalizeHybrid(false),
     m_invertVertical(false),
-    m_rectified(false),
     m_opaque(false),
     m_smooth(false),
     m_peakResolution(256),
     m_miny(0),
-    m_maxy(0)
+    m_maxy(0),
+    m_synchronous(false),
+    m_peakCache(0),
+    m_peakCacheDivisor(8)
 {
-    
+    QSettings settings;
+    settings.beginGroup("Preferences");
+    setColourMap(settings.value("colour-3d-plot-colour", ColourMapper::Green).toInt());
+    settings.endGroup();
 }
 
 Colour3DPlotLayer::~Colour3DPlotLayer()
 {
-    delete m_cache;
-    delete m_peaksCache;
+    invalidateRenderers();
+    delete m_peakCache;
+}
+
+ColourScaleType
+Colour3DPlotLayer::convertToColourScale(int value)
+{
+    switch (value) {
+    default:
+    case 0: return ColourScaleType::Linear;
+    case 1: return ColourScaleType::Log;
+    case 2: return ColourScaleType::PlusMinusOne;
+    case 3: return ColourScaleType::Absolute;
+    }
+}
+
+int
+Colour3DPlotLayer::convertFromColourScale(ColourScaleType scale)
+{
+    switch (scale) {
+    case ColourScaleType::Linear: return 0;
+    case ColourScaleType::Log: return 1;
+    case ColourScaleType::PlusMinusOne: return 2;
+    case ColourScaleType::Absolute: return 3;
+
+    case ColourScaleType::Meter:
+    case ColourScaleType::Phase:
+    default: return 0;
+    }
+}
+
+std::pair<ColumnNormalization, bool>
+Colour3DPlotLayer::convertToColumnNorm(int value)
+{
+    switch (value) {
+    default:
+    case 0: return { ColumnNormalization::None, false };
+    case 1: return { ColumnNormalization::Max1, false };
+    case 2: return { ColumnNormalization::None, true }; // visible area
+    case 3: return { ColumnNormalization::Hybrid, false };
+    }
+}
+
+int
+Colour3DPlotLayer::convertFromColumnNorm(ColumnNormalization norm, bool visible)
+{
+    if (visible) return 2;
+    switch (norm) {
+    case ColumnNormalization::None: return 0;
+    case ColumnNormalization::Max1: return 1;
+    case ColumnNormalization::Hybrid: return 3;
+
+    case ColumnNormalization::Sum1:
+    default: return 0;
+    }
+}
+
+void
+Colour3DPlotLayer::setSynchronousPainting(bool synchronous)
+{
+    m_synchronous = synchronous;
 }
 
 void
@@ -92,7 +158,11 @@
     } else if (model->getResolution() > 2) {
         m_peakResolution = 128;
     }
-    cacheInvalid();
+
+    delete m_peakCache;
+    m_peakCache = 0;
+
+    invalidateRenderers();
 
     emit modelReplaced();
     emit sliceableModelReplaced(oldModel, model);
@@ -101,34 +171,46 @@
 void
 Colour3DPlotLayer::cacheInvalid()
 {
-    delete m_cache;
-    delete m_peaksCache;
-    m_cache = 0;
-    m_peaksCache = 0;
-    m_cacheValidStart = 0;
-    m_cacheValidEnd = 0;
+    invalidateRenderers();
 }
 
 void
-Colour3DPlotLayer::cacheInvalid(sv_frame_t startFrame, sv_frame_t endFrame)
+Colour3DPlotLayer::cacheInvalid(sv_frame_t /* startFrame */,
+                                sv_frame_t /* endFrame */)
 {
-    if (!m_cache || !m_model) return;
+    //!!! should do this only if the range is visible
+    delete m_peakCache;
+    m_peakCache = 0;
+    
+    invalidateRenderers();
+}
 
-    int modelResolution = m_model->getResolution();
-    int start = int(startFrame / modelResolution);
-    int end = int(endFrame / modelResolution + 1);
-    if (m_cacheValidStart < end) m_cacheValidStart = end;
-    if (m_cacheValidEnd > start) m_cacheValidEnd = start;
-    if (m_cacheValidStart > m_cacheValidEnd) m_cacheValidEnd = m_cacheValidStart;
+void
+Colour3DPlotLayer::invalidateRenderers()
+{
+    for (ViewRendererMap::iterator i = m_renderers.begin();
+         i != m_renderers.end(); ++i) {
+        delete i->second;
+    }
+    m_renderers.clear();
+}
+
+Dense3DModelPeakCache *
+Colour3DPlotLayer::getPeakCache() const
+{
+    if (!m_peakCache) {
+        m_peakCache = new Dense3DModelPeakCache(m_model, m_peakCacheDivisor);
+    }
+    return m_peakCache;
 }
 
 void
 Colour3DPlotLayer::modelChanged()
 {
-    if (!m_colourScaleSet && m_colourScale == LinearScale) {
+    if (!m_colourScaleSet && m_colourScale == ColourScaleType::Linear) {
         if (m_model) {
             if (m_model->shouldUseLogValueScale()) {
-                setColourScale(LogScale);
+                setColourScale(ColourScaleType::Log);
             } else {
                 m_colourScaleSet = true;
             }
@@ -140,10 +222,10 @@
 void
 Colour3DPlotLayer::modelChangedWithin(sv_frame_t startFrame, sv_frame_t endFrame)
 {
-    if (!m_colourScaleSet && m_colourScale == LinearScale) {
+    if (!m_colourScaleSet && m_colourScale == ColourScaleType::Linear) {
         if (m_model && m_model->getWidth() > 50) {
             if (m_model->shouldUseLogValueScale()) {
-                setColourScale(LogScale);
+                setColourScale(ColourScaleType::Log);
             } else {
                 m_colourScaleSet = true;
             }
@@ -158,12 +240,10 @@
     PropertyList list;
     list.push_back("Colour");
     list.push_back("Colour Scale");
-    list.push_back("Normalize Columns");
-    list.push_back("Normalize Visible Area");
+    list.push_back("Normalization");
     list.push_back("Gain");
     list.push_back("Bin Scale");
     list.push_back("Invert Vertical Scale");
-    list.push_back("Show Rectified");
     list.push_back("Opaque");
     list.push_back("Smooth");
     return list;
@@ -174,10 +254,8 @@
 {
     if (name == "Colour") return tr("Colour");
     if (name == "Colour Scale") return tr("Scale");
-    if (name == "Normalize Columns") return tr("Normalize Columns");
-    if (name == "Normalize Visible Area") return tr("Normalize Visible Area");
+    if (name == "Normalization") return tr("Normalization");
     if (name == "Invert Vertical Scale") return tr("Invert Vertical Scale");
-    if (name == "Show Rectified") return tr("Half-Wave Rectify");
     if (name == "Gain") return tr("Gain");
     if (name == "Opaque") return tr("Always Opaque");
     if (name == "Smooth") return tr("Smooth");
@@ -188,10 +266,7 @@
 QString
 Colour3DPlotLayer::getPropertyIconName(const PropertyName &name) const
 {
-    if (name == "Normalize Columns") return "normalise-columns";
-    if (name == "Normalize Visible Area") return "normalise";
     if (name == "Invert Vertical Scale") return "invert-vertical";
-    if (name == "Show Rectified") return "derivative";
     if (name == "Opaque") return "opaque";
     if (name == "Smooth") return "smooth";
     return "";
@@ -201,10 +276,7 @@
 Colour3DPlotLayer::getPropertyType(const PropertyName &name) const
 {
     if (name == "Gain") return RangeProperty;
-    if (name == "Normalize Columns") return ToggleProperty;
-    if (name == "Normalize Visible Area") return ToggleProperty;
     if (name == "Invert Vertical Scale") return ToggleProperty;
-    if (name == "Show Rectified") return ToggleProperty;
     if (name == "Opaque") return ToggleProperty;
     if (name == "Smooth") return ToggleProperty;
     return ValueProperty;
@@ -213,10 +285,8 @@
 QString
 Colour3DPlotLayer::getPropertyGroupName(const PropertyName &name) const
 {
-    if (name == "Normalize Columns" ||
-        name == "Normalize Visible Area" ||
-	name == "Colour Scale" ||
-        name == "Show Rectified" ||
+    if (name == "Normalization" ||
+        name == "Colour Scale" ||
         name == "Gain") return tr("Scale");
     if (name == "Bin Scale" ||
         name == "Invert Vertical Scale") return tr("Bins");
@@ -252,11 +322,12 @@
 
     } else if (name == "Colour Scale") {
 
+        // linear, log, +/-1, abs
 	*min = 0;
 	*max = 3;
-        *deflt = (int)LinearScale;
+        *deflt = 0;
 
-	val = (int)m_colourScale;
+	val = convertFromColourScale(m_colourScale);
 
     } else if (name == "Colour") {
 
@@ -266,33 +337,24 @@
 
 	val = m_colourMap;
 
-    } else if (name == "Normalize Columns") {
+    } else if (name == "Normalization") {
 	
+        *min = 0;
+        *max = 3;
         *deflt = 0;
-	val = (m_normalizeColumns ? 1 : 0);
 
-    } else if (name == "Normalize Visible Area") {
-	
-        *deflt = 0;
-	val = (m_normalizeVisibleArea ? 1 : 0);
+        val = convertFromColumnNorm(m_normalization, m_normalizeVisibleArea);
 
     } else if (name == "Invert Vertical Scale") {
 	
         *deflt = 0;
 	val = (m_invertVertical ? 1 : 0);
 
-    } else if (name == "Show Rectified") {
-
-        if (min) *min = 0;
-        if (max) *max = 0;
-        if (deflt) *deflt = 0;
-        val = (m_rectified ? 1.0 : 0.0);
-
     } else if (name == "Bin Scale") {
 
 	*min = 0;
 	*max = 1;
-        *deflt = int(LinearBinScale);
+        *deflt = int(BinScale::Linear);
 	val = (int)m_binScale;
 
     } else if (name == "Opaque") {
@@ -328,6 +390,9 @@
 	case 3: return tr("Absolute");
 	}
     }
+    if (name == "Normalization") {
+        return ""; // icon only
+    }
     if (name == "Bin Scale") {
 	switch (value) {
 	default:
@@ -338,6 +403,22 @@
     return tr("<unknown>");
 }
 
+QString
+Colour3DPlotLayer::getPropertyValueIconName(const PropertyName &name,
+                                            int value) const
+{
+    if (name == "Normalization") {
+        switch(value) {
+        default:
+        case 0: return "normalise-none";
+        case 1: return "normalise-columns";
+        case 2: return "normalise";
+        case 3: return "normalise-hybrid";
+        }
+    }
+    return "";
+}
+
 RangeMapper *
 Colour3DPlotLayer::getNewPropertyRangeMapper(const PropertyName &name) const
 {
@@ -353,23 +434,11 @@
     if (name == "Gain") {
 	setGain(float(pow(10, value/20.0)));
     } else if (name == "Colour Scale") {
-	switch (value) {
-	default:
-	case 0: setColourScale(LinearScale); break;
-	case 1: setColourScale(LogScale); break;
-	case 2: setColourScale(PlusMinusOneScale); break;
-	case 3: setColourScale(AbsoluteScale); break;
-	}
+        setColourScale(convertToColourScale(value));
     } else if (name == "Colour") {
         setColourMap(value);
-    } else if (name == "Normalize Columns") {
-	setNormalizeColumns(value ? true : false);
-    } else if (name == "Normalize Visible Area") {
-	setNormalizeVisibleArea(value ? true : false);
     } else if (name == "Invert Vertical Scale") {
 	setInvertVertical(value ? true : false);
-    } else if (name == "Show Rectified") {
-        setShowRectified(value > 0.5);
     } else if (name == "Opaque") {
 	setOpaque(value ? true : false);
     } else if (name == "Smooth") {
@@ -377,19 +446,23 @@
     } else if (name == "Bin Scale") {
 	switch (value) {
 	default:
-	case 0: setBinScale(LinearBinScale); break;
-	case 1: setBinScale(LogBinScale); break;
+	case 0: setBinScale(BinScale::Linear); break;
+	case 1: setBinScale(BinScale::Log); break;
 	}
+    } else if (name == "Normalization") {
+        auto n = convertToColumnNorm(value);
+        setNormalization(n.first);
+        setNormalizeVisibleArea(n.second);
     }
 }
 
 void
-Colour3DPlotLayer::setColourScale(ColourScale scale)
+Colour3DPlotLayer::setColourScale(ColourScaleType scale)
 {
     if (m_colourScale == scale) return;
     m_colourScale = scale;
     m_colourScaleSet = true;
-    cacheInvalid();
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
@@ -398,7 +471,7 @@
 {
     if (m_colourMap == map) return;
     m_colourMap = map;
-    cacheInvalid();
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
@@ -407,7 +480,7 @@
 {
     if (m_gain == gain) return;
     m_gain = gain;
-    cacheInvalid();
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
@@ -422,52 +495,41 @@
 {
     if (m_binScale == binScale) return;
     m_binScale = binScale;
-    cacheInvalid();
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
-Colour3DPlotLayer::BinScale
+BinScale
 Colour3DPlotLayer::getBinScale() const
 {
     return m_binScale;
 }
 
 void
-Colour3DPlotLayer::setNormalizeColumns(bool n)
+Colour3DPlotLayer::setNormalization(ColumnNormalization n)
 {
-    if (m_normalizeColumns == n) return;
-    m_normalizeColumns = n;
-    cacheInvalid();
+    if (m_normalization == n) return;
+
+    m_normalization = n;
+    invalidateRenderers();
+    
     emit layerParametersChanged();
 }
 
-bool
-Colour3DPlotLayer::getNormalizeColumns() const
+ColumnNormalization
+Colour3DPlotLayer::getNormalization() const
 {
-    return m_normalizeColumns;
-}
-
-void
-Colour3DPlotLayer::setNormalizeHybrid(bool n)
-{
-    if (m_normalizeHybrid == n) return;
-    m_normalizeHybrid = n;
-    cacheInvalid();
-    emit layerParametersChanged();
-}
-
-bool
-Colour3DPlotLayer::getNormalizeHybrid() const
-{
-    return m_normalizeHybrid;
+    return m_normalization;
 }
 
 void
 Colour3DPlotLayer::setNormalizeVisibleArea(bool n)
 {
     if (m_normalizeVisibleArea == n) return;
+
     m_normalizeVisibleArea = n;
-    cacheInvalid();
+    invalidateRenderers();
+    
     emit layerParametersChanged();
 }
 
@@ -482,16 +544,7 @@
 {
     if (m_invertVertical == n) return;
     m_invertVertical = n;
-    cacheInvalid();
-    emit layerParametersChanged();
-}
-
-void
-Colour3DPlotLayer::setShowRectified(bool show)
-{
-    if (m_rectified == show) return;
-    m_rectified = show;
-    cacheInvalid();
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
@@ -500,6 +553,7 @@
 {
     if (m_opaque == n) return;
     m_opaque = n;
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
@@ -508,6 +562,7 @@
 {
     if (m_smooth == n) return;
     m_smooth = n;
+    invalidateRenderers();
     emit layerParametersChanged();
 }
 
@@ -554,16 +609,19 @@
 }
 
 bool
-Colour3DPlotLayer::isLayerScrollable(const LayerGeometryProvider *v) const
+Colour3DPlotLayer::isLayerScrollable(const LayerGeometryProvider */* v */) const
 {
     if (m_normalizeVisibleArea) {
         return false;
     }
-    if (shouldPaintDenseIn(v)) {
-        return true;
-    }
-    QPoint discard;
-    return !v->shouldIlluminateLocalFeatures(this, discard);
+    //!!! ah hang on, if we're potentially rendering incrementally
+    //!!! they we can't be scrollable
+    return false;
+//    if (getRenderer(v)->willRenderOpaque(v)) {
+//        return true;
+//    }
+//    QPoint discard;
+//    return !v->shouldIlluminateLocalFeatures(this, discard);
 }
 
 bool
@@ -608,6 +666,8 @@
     m_miny = int(lrint(min));
     m_maxy = int(lrint(max));
     
+    invalidateRenderers();
+    
     emit layerParametersChanged();
     return true;
 }
@@ -654,6 +714,8 @@
     m_maxy = m_miny + dist;
     if (m_maxy > m_model->getHeight()) m_maxy = m_model->getHeight();
 
+    invalidateRenderers();
+    
 //    SVDEBUG << "Colour3DPlotLayer::setVerticalZoomStep(" <<step <<"):  after: miny = " << m_miny << ", maxy = " << m_maxy << endl;
     
     emit layerParametersChanged();
@@ -669,14 +731,14 @@
 }
 
 double
-Colour3DPlotLayer::getYForBin(LayerGeometryProvider *v, double bin) const
+Colour3DPlotLayer::getYForBin(const LayerGeometryProvider *v, double bin) const
 {
     double y = bin;
     if (!m_model) return y;
     double mn = 0, mx = m_model->getHeight();
     getDisplayExtents(mn, mx);
     double h = v->getPaintHeight();
-    if (m_binScale == LinearBinScale) {
+    if (m_binScale == BinScale::Linear) {
         y = h - (((bin - mn) * h) / (mx - mn));
     } else {
         double logmin = mn + 1, logmax = mx + 1;
@@ -686,21 +748,15 @@
     return y;
 }
 
-int
-Colour3DPlotLayer::getIYForBin(LayerGeometryProvider *v, int bin) const
-{
-    return int(round(getYForBin(v, bin)));
-}
-
 double
-Colour3DPlotLayer::getBinForY(LayerGeometryProvider *v, double y) const
+Colour3DPlotLayer::getBinForY(const LayerGeometryProvider *v, double y) const
 {
     double bin = y;
     if (!m_model) return bin;
     double mn = 0, mx = m_model->getHeight();
     getDisplayExtents(mn, mx);
     double h = v->getPaintHeight();
-    if (m_binScale == LinearBinScale) {
+    if (m_binScale == BinScale::Linear) {
         bin = mn + ((h - y) * (mx - mn)) / h;
     } else {
         double logmin = mn + 1, logmax = mx + 1;
@@ -710,12 +766,6 @@
     return bin;
 }
 
-int
-Colour3DPlotLayer::getIBinForY(LayerGeometryProvider *v, int y) const
-{
-    return int(floor(getBinForY(v, y)));
-}
-
 QString
 Colour3DPlotLayer::getFeatureDescription(LayerGeometryProvider *v, QPoint &pos) const
 {
@@ -818,49 +868,20 @@
     int cw = getColourScaleWidth(paint);
     
     int ch = h - 20;
-    if (ch > 20 && m_cache) {
+    if (ch > 20) {
 
-        double min = m_model->getMinimumLevel();
-        double max = m_model->getMaximumLevel();
+        double min = m_viewMags[v->getId()].getMin();
+        double max = m_viewMags[v->getId()].getMax();
 
-        double mmin = min;
-        double mmax = max;
+        if (max <= min) max = min + 0.1;
 
-        if (m_colourScale == LogScale) {
-            LogRange::mapRange(mmin, mmax);
-        } else if (m_colourScale == PlusMinusOneScale) {
-            mmin = -1.f;
-            mmax = 1.f;
-        } else if (m_colourScale == AbsoluteScale) {
-            if (mmin < 0) {
-                if (fabs(mmin) > fabs(mmax)) mmax = fabs(mmin);
-                else mmax = fabs(mmax);
-                mmin = 0;
-            } else {
-                mmin = fabs(mmin);
-                mmax = fabs(mmax);
-            }
-        }
-    
-        if (max == min) max = min + 1.f;
-        if (mmax == mmin) mmax = mmin + 1.f;
-    
         paint.setPen(v->getForeground());
         paint.drawRect(4, 10, cw - 8, ch+1);
 
         for (int y = 0; y < ch; ++y) {
             double value = ((max - min) * (double(ch-y) - 1.0)) / double(ch) + min;
-            if (m_colourScale == LogScale) {
-                value = LogRange::map(value);
-            }
-            int pixel = int(((value - mmin) * 256) / (mmax - mmin));
-            if (pixel >= 0 && pixel < 256) {
-                QRgb c = m_cache->color(pixel);
-                paint.setPen(QColor(qRed(c), qGreen(c), qBlue(c)));
-                paint.drawLine(5, 11 + y, cw - 5, 11 + y);
-            } else {
-                cerr << "WARNING: Colour3DPlotLayer::paintVerticalScale: value " << value << ", mmin " << mmin << ", mmax " << mmax << " leads to invalid pixel " << pixel << endl;
-            }
+            paint.setPen(getRenderer(v)->getColour(value));
+            paint.drawLine(5, 11 + y, cw - 5, 11 + y);
         }
 
         QString minstr = QString("%1").arg(min);
@@ -869,8 +890,12 @@
         paint.save();
 
         QFont font = paint.font();
-        font.setPixelSize(int(font.pixelSize() * 0.65));
-        paint.setFont(font);
+        if (font.pixelSize() > 0) {
+            int newSize = int(font.pixelSize() * 0.65);
+            if (newSize < 6) newSize = 6;
+            font.setPixelSize(newSize);
+            paint.setFont(font);
+        }
 
         int msw = paint.fontMetrics().width(maxstr);
 
@@ -880,12 +905,12 @@
 
         paint.setWorldMatrix(m);
 
-        v->drawVisibleText(paint, 2, 0, minstr, View::OutlinedText);
+        PaintAssistant::drawVisibleText(v, paint, 2, 0, minstr, PaintAssistant::OutlinedText);
 
         m.translate(ch - msw - 2, 0);
         paint.setWorldMatrix(m);
 
-        v->drawVisibleText(paint, 0, 0, maxstr, View::OutlinedText);
+        PaintAssistant::drawVisibleText(v, paint, 0, 0, maxstr, PaintAssistant::OutlinedText);
 
         paint.restore();
     }
@@ -957,36 +982,28 @@
 {
     Profiler profiler("Colour3DPlotLayer::getColumn");
 
-    DenseThreeDimensionalModel::Column prev;
-    if (m_rectified && (col > m_model->getStartFrame())) {
-        prev = m_model->getColumn(col - 1);
+    DenseThreeDimensionalModel::Column values = m_model->getColumn(col);
+    values.resize(m_model->getHeight(), 0.f);
+    if (m_normalization != ColumnNormalization::Max1 &&
+        m_normalization != ColumnNormalization::Hybrid) {
+        return values;
     }
-    
-    DenseThreeDimensionalModel::Column values = m_model->getColumn(col);
-
-    if (m_rectified && !prev.empty()) {
-        for (int y = 0; y < values.size(); ++y) {
-            if (values[y] < prev[y]) values[y] = 0;
-            else values[y] -= prev[y];
-        }
-    }
-    
-    while (values.size() < m_model->getHeight()) values.push_back(0.f);
-    if (!m_normalizeColumns && !m_normalizeHybrid) return values;
 
     double colMax = 0.f, colMin = 0.f;
     double min = 0.f, max = 0.f;
 
+    int nv = int(values.size());
+    
     min = m_model->getMinimumLevel();
     max = m_model->getMaximumLevel();
 
-    for (int y = 0; y < values.size(); ++y) {
+    for (int y = 0; y < nv; ++y) {
         if (y == 0 || values.at(y) > colMax) colMax = values.at(y);
         if (y == 0 || values.at(y) < colMin) colMin = values.at(y);
     }
     if (colMin == colMax) colMax = colMin + 1;
     
-    for (int y = 0; y < values.size(); ++y) {
+    for (int y = 0; y < nv; ++y) {
     
         double value = values.at(y);
         double norm = (value - colMin) / (colMax - colMin);
@@ -995,311 +1012,102 @@
         if (value != newvalue) values[y] = float(newvalue);
     }
 
-    if (m_normalizeHybrid && (colMax > 0.0)) {
+    if (m_normalization == ColumnNormalization::Hybrid
+        && (colMax > 0.0)) {
         double logmax = log10(colMax);
-        for (int y = 0; y < values.size(); ++y) {
+        for (int y = 0; y < nv; ++y) {
             values[y] = float(values[y] * logmax);
         }
     }
 
     return values;
 }
-    
-void
-Colour3DPlotLayer::fillCache(int firstBin, int lastBin) const
+
+Colour3DPlotRenderer *
+Colour3DPlotLayer::getRenderer(const LayerGeometryProvider *v) const
 {
-    Profiler profiler("Colour3DPlotLayer::fillCache", true);
+    if (m_renderers.find(v->getId()) == m_renderers.end()) {
 
-    sv_frame_t modelStart = m_model->getStartFrame();
-    sv_frame_t modelEnd = m_model->getEndFrame();
-    int modelResolution = m_model->getResolution();
+        Colour3DPlotRenderer::Sources sources;
+        sources.verticalBinLayer = this;
+        sources.fft = 0;
+        sources.source = m_model;
+        sources.peaks = getPeakCache();
 
-    int modelStartBin = int(modelStart / modelResolution);
-    int modelEndBin = int(modelEnd / modelResolution);
+        ColourScale::Parameters cparams;
+        cparams.colourMap = m_colourMap;
+        cparams.scaleType = m_colourScale;
+        cparams.gain = m_gain;
 
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-    cerr << "Colour3DPlotLayer::fillCache: range " << firstBin << " -> " << lastBin << " of model range " << modelStartBin << " -> " << modelEndBin << " (model resolution " << modelResolution << ")" << endl;
-#endif
+        if (m_normalization == ColumnNormalization::None) {
+            cparams.minValue = m_model->getMinimumLevel();
+            cparams.maxValue = m_model->getMaximumLevel();
+        } else if (m_normalization == ColumnNormalization::Hybrid) {
+            cparams.minValue = 0;
+            cparams.maxValue = log10(m_model->getMaximumLevel() + 1.0);
+        }
 
-    int cacheWidth = modelEndBin - modelStartBin + 1;
-    if (lastBin > modelEndBin) cacheWidth = lastBin - modelStartBin + 1;
-    int cacheHeight = m_model->getHeight();
+        if (cparams.maxValue <= cparams.minValue) {
+            cparams.maxValue = cparams.minValue + 0.1;
+        }
+        
+        Colour3DPlotRenderer::Parameters params;
+        params.colourScale = ColourScale(cparams);
+        params.normalization = m_normalization;
+        params.binScale = m_binScale;
+        params.alwaysOpaque = m_opaque;
+        params.invertVertical = m_invertVertical;
+        params.interpolate = m_smooth;
 
-    if (m_cache && m_cache->height() != cacheHeight) {
-        // height has changed: delete everything rather than resizing
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-        cerr << "Colour3DPlotLayer::fillCache: Cache height has changed, recreating" << endl;
-#endif
-        delete m_cache;
-        delete m_peaksCache;
-        m_cache = 0;
-        m_peaksCache = 0;
-    } 
-
-    if (m_cache && m_cache->width() != cacheWidth) {
-        // width has changed and we have an existing cache: resize it
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-        cerr << "Colour3DPlotLayer::fillCache: Cache width has changed, resizing existing cache" << endl;
-#endif
-        QImage *newCache =
-            new QImage(m_cache->copy(0, 0, cacheWidth, cacheHeight));
-        delete m_cache;
-        m_cache = newCache;
-        if (m_peaksCache) {
-            QImage *newPeaksCache =
-                new QImage(m_peaksCache->copy
-                           (0, 0, cacheWidth / m_peakResolution + 1, cacheHeight));
-            delete m_peaksCache;
-            m_peaksCache = newPeaksCache;
-        }
+        m_renderers[v->getId()] = new Colour3DPlotRenderer(sources, params);
     }
 
-    if (!m_cache) {
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-        cerr << "Colour3DPlotLayer::fillCache: Have no cache, making one" << endl;
-#endif
-        m_cache = new QImage
-            (cacheWidth, cacheHeight, QImage::Format_Indexed8);
-        m_cache->setColorCount(256);
-        m_cache->fill(0);
-        if (!m_normalizeVisibleArea) {
-            m_peaksCache = new QImage
-                (cacheWidth / m_peakResolution + 1, cacheHeight,
-                 QImage::Format_Indexed8);
-            m_peaksCache->setColorCount(256);
-            m_peaksCache->fill(0);
-        } else if (m_peaksCache) {
-            delete m_peaksCache;
-            m_peaksCache = 0;
-        }
-        m_cacheValidStart = 0;
-        m_cacheValidEnd = 0;
-    }
+    return m_renderers[v->getId()];
+}
 
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-    cerr << "cache size = " << m_cache->width() << "x" << m_cache->height()
-         << " peaks cache size = " << m_peaksCache->width() << "x" << m_peaksCache->height() << endl;
-#endif
+void
+Colour3DPlotLayer::paintWithRenderer(LayerGeometryProvider *v,
+                                     QPainter &paint, QRect rect) const
+{
+    Colour3DPlotRenderer *renderer = getRenderer(v);
 
-    if (m_cacheValidStart <= firstBin && m_cacheValidEnd >= lastBin) {
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-        cerr << "Cache is valid in this region already" << endl;
-#endif
-        return;
+    Colour3DPlotRenderer::RenderResult result;
+    MagnitudeRange magRange;
+    int viewId = v->getId();
+
+    if (!renderer->geometryChanged(v)) {
+        magRange = m_viewMags[viewId];
     }
     
-    int fillStart = firstBin;
-    int fillEnd = lastBin;
+    if (m_synchronous) {
 
-    if (fillStart < modelStartBin) fillStart = modelStartBin;
-    if (fillStart > modelEndBin) fillStart = modelEndBin;
-    if (fillEnd < modelStartBin) fillEnd = modelStartBin;
-    if (fillEnd > modelEndBin) fillEnd = modelEndBin;
-
-    bool normalizeVisible = (m_normalizeVisibleArea && !m_normalizeColumns);
-
-    if (!normalizeVisible && (m_cacheValidStart < m_cacheValidEnd)) {
-        
-        if (m_cacheValidEnd < fillStart) {
-            fillStart = m_cacheValidEnd + 1;
-        }
-        if (m_cacheValidStart > fillEnd) {
-            fillEnd = m_cacheValidStart - 1;
-        }
-        
-        m_cacheValidStart = std::min(fillStart, m_cacheValidStart);
-        m_cacheValidEnd = std::max(fillEnd, m_cacheValidEnd);
+        result = renderer->render(v, paint, rect);
 
     } else {
 
-        // when normalising the visible area, the only valid area,
-        // ever, is the currently visible one
+        result = renderer->renderTimeConstrained(v, paint, rect);
 
-        m_cacheValidStart = fillStart;
-        m_cacheValidEnd = fillEnd;
-    }
-
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-    cerr << "Cache size " << cacheWidth << "x" << cacheHeight << " will be valid from " << m_cacheValidStart << " to " << m_cacheValidEnd << " (fillStart = " << fillStart << ", fillEnd = " << fillEnd << ")" << endl;
-#endif
-
-    DenseThreeDimensionalModel::Column values;
-
-    double min = m_model->getMinimumLevel();
-    double max = m_model->getMaximumLevel();
-
-    if (m_colourScale == LogScale) {
-        LogRange::mapRange(min, max);
-    } else if (m_colourScale == PlusMinusOneScale) {
-        min = -1.f;
-        max = 1.f;
-    } else if (m_colourScale == AbsoluteScale) {
-        if (min < 0) {
-            if (fabs(min) > fabs(max)) max = fabs(min);
-            else max = fabs(max);
-            min = 0;
-        } else {
-            min = fabs(min);
-            max = fabs(max);
+        QRect uncached = renderer->getLargestUncachedRect(v);
+        if (uncached.width() > 0) {
+            v->updatePaintRect(uncached);
         }
     }
     
-    if (max == min) max = min + 1.f;
-    
-    ColourMapper mapper(m_colourMap, 0.f, 255.f);
-    
-    for (int index = 0; index < 256; ++index) {
-        QColor colour = mapper.map(index);
-        m_cache->setColor
-            (index, qRgb(colour.red(), colour.green(), colour.blue()));
-        if (m_peaksCache) {
-            m_peaksCache->setColor
-                (index, qRgb(colour.red(), colour.green(), colour.blue()));
+    magRange.sample(result.range);
+
+    if (magRange.isSet()) {
+        if (!(m_viewMags[viewId] == magRange)) {
+            m_viewMags[viewId] = magRange;
+    //!!! now need to do the normalise-visible thing
         }
     }
     
-    double visibleMax = 0.f, visibleMin = 0.f;
-
-    if (normalizeVisible) {
+    cerr << "mag range in this view: "
+         << m_viewMags[v->getId()].getMin()
+         << " -> "
+         << m_viewMags[v->getId()].getMax()
+         << endl;
         
-        for (int c = fillStart; c <= fillEnd; ++c) {
-	
-            values = getColumn(c);
-
-            double colMax = 0.f, colMin = 0.f;
-
-            for (int y = 0; y < cacheHeight; ++y) {
-                if (y >= values.size()) break;
-                if (y == 0 || values[y] > colMax) colMax = values[y];
-                if (y == 0 || values[y] < colMin) colMin = values[y];
-            }
-
-            if (c == fillStart || colMax > visibleMax) visibleMax = colMax;
-            if (c == fillStart || colMin < visibleMin) visibleMin = colMin;
-        }
-
-        if (m_colourScale == LogScale) {
-            visibleMin = LogRange::map(visibleMin);
-            visibleMax = LogRange::map(visibleMax);
-            if (visibleMin > visibleMax) std::swap(visibleMin, visibleMax);
-        } else if (m_colourScale == AbsoluteScale) {
-            if (visibleMin < 0) {
-                if (fabs(visibleMin) > fabs(visibleMax)) visibleMax = fabs(visibleMin);
-                else visibleMax = fabs(visibleMax);
-                visibleMin = 0;
-            } else {
-                visibleMin = fabs(visibleMin);
-                visibleMax = fabs(visibleMax);
-            }
-        }
-    }
-    
-    if (visibleMin == visibleMax) visibleMax = visibleMin + 1;
-
-    int *peaks = 0;
-    if (m_peaksCache) {
-        peaks = new int[cacheHeight];
-        for (int y = 0; y < cacheHeight; ++y) {
-            peaks[y] = 0;
-        }
-    }
-
-    Profiler profiler2("Colour3DPlotLayer::fillCache: filling", true);
-
-    for (int c = fillStart; c <= fillEnd; ++c) {
-	
-        values = getColumn(c);
-
-        if (c >= m_cache->width()) {
-            cerr << "ERROR: column " << c << " >= cache width "
-                 << m_cache->width() << endl;
-            continue;
-        }
-
-        for (int y = 0; y < cacheHeight; ++y) {
-
-            double value = min;
-            if (y < values.size()) {
-                value = values.at(y);
-            }
-
-            value = value * m_gain;
-
-            if (m_colourScale == LogScale) {
-                value = LogRange::map(value);
-            } else if (m_colourScale == AbsoluteScale) {
-                value = fabs(value);
-            }
-            
-            if (normalizeVisible) {
-                double norm = (value - visibleMin) / (visibleMax - visibleMin);
-                value = min + (max - min) * norm;
-            }
-
-            int pixel = int(((value - min) * 256) / (max - min));
-            if (pixel < 0) pixel = 0;
-            if (pixel > 255) pixel = 255;
-            if (peaks && (pixel > peaks[y])) peaks[y] = pixel;
-
-            if (m_invertVertical) {
-                m_cache->setPixel(c, cacheHeight - y - 1, pixel);
-            } else {
-                if (y >= m_cache->height()) {
-                    cerr << "ERROR: row " << y << " >= cache height " << m_cache->height() << endl;
-                } else {
-                    m_cache->setPixel(c, y, pixel);
-                }
-            }
-        }
-
-        if (peaks) {
-            int notch = (c % m_peakResolution);
-            if (notch == m_peakResolution-1 || c == fillEnd) {
-                int pc = c / m_peakResolution;
-                if (pc >= m_peaksCache->width()) {
-                    cerr << "ERROR: peak column " << pc
-                         << " (from col " << c << ") >= peaks cache width "
-                         << m_peaksCache->width() << endl;
-                    continue;
-                }
-                for (int y = 0; y < cacheHeight; ++y) {
-                    if (m_invertVertical) {
-                        m_peaksCache->setPixel(pc, cacheHeight - y - 1, peaks[y]);
-                    } else {
-                        if (y >= m_peaksCache->height()) {
-                            cerr << "ERROR: row " << y
-                                 << " >= peaks cache height "
-                                 << m_peaksCache->height() << endl;
-                        } else {
-                            m_peaksCache->setPixel(pc, y, peaks[y]);
-                        }
-                    }
-                }
-                for (int y = 0; y < cacheHeight; ++y) {
-                    peaks[y] = 0;
-                }
-            }
-        }
-    }
-
-    delete[] peaks;
-}
-
-bool
-Colour3DPlotLayer::shouldPaintDenseIn(const LayerGeometryProvider *v) const
-{
-    if (!m_model || !v || !(v->getViewManager())) {
-        return false;
-    }
-    double srRatio =
-        v->getViewManager()->getMainModelSampleRate() / m_model->getSampleRate();
-    if (m_opaque || 
-        m_smooth ||
-        m_model->getHeight() >= v->getPaintHeight() ||
-        ((m_model->getResolution() * srRatio) / v->getZoomLevel()) < 2) {
-        return true;
-    }
-    return false;
 }
 
 void
@@ -1324,374 +1132,25 @@
 	return;
     }
 
-    if (m_normalizeVisibleArea && !m_normalizeColumns) rect = v->getPaintRect();
-
-    sv_frame_t modelStart = m_model->getStartFrame();
-    sv_frame_t modelEnd = m_model->getEndFrame();
-    int modelResolution = m_model->getResolution();
-
-    // The cache is from the model's start frame to the model's end
-    // frame at the model's window increment frames per pixel.  We
-    // want to draw from our start frame + x0 * zoomLevel to our start
-    // frame + x1 * zoomLevel at zoomLevel frames per pixel.
-
-    //  We have quite different paint mechanisms for rendering "large"
-    //  bins (more than one bin per pixel in both directions) and
-    //  "small".  This is "large"; see paintDense below for "small".
-
-    int x0 = rect.left();
-    int x1 = rect.right() + 1;
-
-    int h = v->getPaintHeight();
-
-    double srRatio =
-        v->getViewManager()->getMainModelSampleRate() / m_model->getSampleRate();
-
-    int sx0 = int((double(v->getFrameForX(x0)) / srRatio - double(modelStart))
-                  / modelResolution);
-    int sx1 = int((double(v->getFrameForX(x1)) / srRatio - double(modelStart))
-                  / modelResolution);
-    int sh = m_model->getHeight();
-
-    int symin = m_miny;
-    int symax = m_maxy;
-    if (symax <= symin) {
-        symin = 0;
-        symax = sh;
-    }
-    if (symin < 0) symin = 0;
-    if (symax > sh) symax = sh;
-
-    if (sx0 > 0) --sx0;
-    fillCache(sx0 < 0 ? 0 : sx0,
-              sx1 < 0 ? 0 : sx1);
-
+    if (m_model->getWidth() == 0) {
 #ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-    cerr << "Colour3DPlotLayer::paint: height = "<< m_model->getHeight() << ", modelStart = " << modelStart << ", resolution = " << modelResolution << ", model rate = " << m_model->getSampleRate() << " (zoom level = " << v->getZoomLevel() << ", srRatio = " << srRatio << ")" << endl;
+        cerr << "Colour3DPlotLayer::paint(): model width == 0, "
+             << "nothing to paint (yet)" << endl;
 #endif
-
-    if (shouldPaintDenseIn(v)) {
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-        cerr << "calling paintDense" << endl;
-#endif
-        paintDense(v, paint, rect);
         return;
     }
 
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-    cerr << "Colour3DPlotLayer::paint: w " << x1-x0 << ", h " << h << ", sx0 " << sx0 << ", sx1 " << sx1 << ", sw " << sx1-sx0 << ", sh " << sh << endl;
-    cerr << "Colour3DPlotLayer: sample rate is " << m_model->getSampleRate() << ", resolution " << m_model->getResolution() << endl;
-#endif
-
-    QPoint illuminatePos;
-    bool illuminate = v->shouldIlluminateLocalFeatures(this, illuminatePos);
+    //!!!???
     
-    const int buflen = 40;
-    char labelbuf[buflen];
-
-    for (int sx = sx0; sx <= sx1; ++sx) {
-
-	sv_frame_t fx = sx * modelResolution;
-
-	if (fx + modelResolution <= modelStart || fx > modelEnd) continue;
-
-        int rx0 = v->getXForFrame(int(double(fx + modelStart) * srRatio));
-	int rx1 = v->getXForFrame(int(double(fx + modelStart + modelResolution + 1) * srRatio));
-
-	int rw = rx1 - rx0;
-	if (rw < 1) rw = 1;
-
-	bool showLabel = (rw > 10 &&
-			  paint.fontMetrics().width("0.000000") < rw - 3 &&
-			  paint.fontMetrics().height() < (h / sh));
-        
-	for (int sy = symin; sy < symax; ++sy) {
-
-            int ry0 = getIYForBin(v, sy);
-            int ry1 = getIYForBin(v, sy + 1);
-            QRect r(rx0, ry1, rw, ry0 - ry1);
-
-	    QRgb pixel = qRgb(255, 255, 255);
-	    if (sx >= 0 && sx < m_cache->width() &&
-		sy >= 0 && sy < m_cache->height()) {
-		pixel = m_cache->pixel(sx, sy);
-	    }
-
-            if (rw == 1) {
-                paint.setPen(pixel);
-                paint.setBrush(Qt::NoBrush);
-                paint.drawLine(r.x(), r.y(), r.x(), r.y() + r.height() - 1);
-                continue;
-            }
-
-	    QColor pen(255, 255, 255, 80);
-	    QColor brush(pixel);
-
-            if (rw > 3 && r.height() > 3) {
-                brush.setAlpha(160);
-            }
-
-	    paint.setPen(Qt::NoPen);
-	    paint.setBrush(brush);
-
-	    if (illuminate) {
-		if (r.contains(illuminatePos)) {
-		    paint.setPen(v->getForeground());
-		}
-	    }
-            
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-//            cerr << "rect " << r.x() << "," << r.y() << " "
-//                      << r.width() << "x" << r.height() << endl;
-#endif
-
-	    paint.drawRect(r);
-
-	    if (showLabel) {
-		if (sx >= 0 && sx < m_cache->width() &&
-		    sy >= 0 && sy < m_cache->height()) {
-		    double value = m_model->getValueAt(sx, sy);
-		    snprintf(labelbuf, buflen, "%06f", value);
-		    QString text(labelbuf);
-		    paint.setPen(v->getBackground());
-		    paint.drawText(rx0 + 2,
-				   ry0 - h / sh - 1 + 2 + paint.fontMetrics().ascent(),
-				   text);
-		}
-	    }
-	}
-    }
-}
-
-void
-Colour3DPlotLayer::paintDense(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
-{
-    Profiler profiler("Colour3DPlotLayer::paintDense", true);
-    if (!m_cache) return;
-
-    double modelStart = double(m_model->getStartFrame());
-    double modelResolution = double(m_model->getResolution());
-
-    sv_samplerate_t mmsr = v->getViewManager()->getMainModelSampleRate();
-    sv_samplerate_t msr = m_model->getSampleRate();
-    double srRatio = mmsr / msr;
-
-    int x0 = rect.left();
-    int x1 = rect.right() + 1;
-
-    const int w = x1 - x0; // const so it can be used as array size below
-    int h = v->getPaintHeight(); // we always paint full height
-    int sh = m_model->getHeight();
-
-    int symin = m_miny;
-    int symax = m_maxy;
-    if (symax <= symin) {
-        symin = 0;
-        symax = sh;
-    }
-    if (symin < 0) symin = 0;
-    if (symax > sh) symax = sh;
-
-    QImage img(w, h, QImage::Format_Indexed8);
-    img.setColorTable(m_cache->colorTable());
-
-    uchar *peaks = new uchar[w];
-    memset(peaks, 0, w);
-
-    int zoomLevel = v->getZoomLevel();
-    
-    QImage *source = m_cache;
-
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT    
-    cerr << "modelResolution " << modelResolution << ", srRatio "
-         << srRatio << ", m_peakResolution " << m_peakResolution
-         << ", zoomLevel " << zoomLevel << ", result "
-         << ((modelResolution * srRatio * m_peakResolution) / zoomLevel)
-         << endl;
-#endif
-
-    if (m_peaksCache) {
-        if (((modelResolution * srRatio * m_peakResolution) / zoomLevel) < 1) {
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT    
-            cerr << "using peaks cache" << endl;
-#endif
-            source = m_peaksCache;
-            modelResolution *= m_peakResolution;
-        } else {
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT    
-            cerr << "not using peaks cache" << endl;
-#endif
-        }
-    } else {
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT    
-        cerr << "have no peaks cache" << endl;
-#endif
+    if (m_normalizeVisibleArea) {
+        rect = v->getPaintRect();
     }
 
-    int sw = source->width();
-    
-    sv_frame_t xf = -1;
-    sv_frame_t nxf = v->getFrameForX(x0);
+    //!!! why is the setLayerDormant(false) found here in
+    //!!! SpectrogramLayer not present in Colour3DPlotLayer?
+    //!!! unnecessary? vestigial? forgotten?
 
-    double epsilon = 0.000001;
-
-    vector<double> sxa(w*2);
-    
-    for (int x = 0; x < w; ++x) {
-
-        xf = nxf;
-        nxf = xf + zoomLevel;
-
-        double sx0 = (double(xf) / srRatio - modelStart) / modelResolution;
-        double sx1 = (double(nxf) / srRatio - modelStart) / modelResolution;
-
-        sxa[x*2] = sx0;
-        sxa[x*2 + 1] = sx1;
-    }
-
-    double logmin = symin+1, logmax = symax+1;
-    LogRange::mapRange(logmin, logmax);
-
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-    cerr << "m_smooth = " << m_smooth << ", w = " << w << ", h = " << h << endl;
-#endif
-
-    if (m_smooth) {
-        
-        for (int y = 0; y < h; ++y) {
-
-            double sy = getBinForY(v, y) - 0.5;
-            int syi = int(sy + epsilon);
-            if (syi < 0 || syi >= source->height()) continue;
-
-            uchar *targetLine = img.scanLine(y);
-            uchar *sourceLine = source->scanLine(syi);
-            uchar *nextSource;
-            if (syi + 1 < source->height()) {
-                nextSource = source->scanLine(syi + 1);
-            } else {
-                nextSource = sourceLine;
-            }
-
-            for (int x = 0; x < w; ++x) {
-
-                targetLine[x] = 0;
-
-                double sx0 = sxa[x*2];
-                if (sx0 < 0) continue;
-                int sx0i = int(sx0 + epsilon);
-                if (sx0i >= sw) break;
-
-                double a = sourceLine[sx0i];
-                double b = a;
-                double value;
-
-                double sx1 = sxa[x*2+1];
-                if (sx1 > sx0 + 1.f) {
-                    int sx1i = int(sx1);
-                    bool have = false;
-                    for (int sx = sx0i; sx <= sx1i; ++sx) {
-                        if (sx < 0 || sx >= sw) continue;
-                        if (!have) {
-                            a = sourceLine[sx];
-                            b = nextSource[sx];
-                            have = true;
-                        } else {
-                            a = std::max(a, double(sourceLine[sx]));
-                            b = std::max(b, double(nextSource[sx]));
-                        }
-                    }
-                    double yprop = sy - syi;
-                    value = (a * (1.f - yprop) + b * yprop);
-                } else {
-                    a = sourceLine[sx0i];
-                    b = nextSource[sx0i];
-                    double yprop = sy - syi;
-                    value = (a * (1.f - yprop) + b * yprop);
-                    int oi = sx0i + 1;
-                    double xprop = sx0 - sx0i;
-                    xprop -= 0.5;
-                    if (xprop < 0) {
-                        oi = sx0i - 1;
-                        xprop = -xprop;
-                    }
-                    if (oi < 0 || oi >= sw) oi = sx0i;
-                    a = sourceLine[oi];
-                    b = nextSource[oi];
-                    value = (value * (1.f - xprop) +
-                             (a * (1.f - yprop) + b * yprop) * xprop);
-                }
-                
-                int vi = int(lrint(value));
-                if (vi > 255) vi = 255;
-                if (vi < 0) vi = 0;
-                targetLine[x] = uchar(vi);
-            }
-        }
-    } else {
-
-        double sy0 = getBinForY(v, 0);
-
-        int psy0i = -1, psy1i = -1;
-
-        for (int y = 0; y < h; ++y) {
-
-            double sy1 = sy0;
-            sy0 = getBinForY(v, double(y + 1));
-
-            int sy0i = int(sy0 + epsilon);
-            int sy1i = int(sy1);
-
-            uchar *targetLine = img.scanLine(y);
-
-            if (sy0i == psy0i && sy1i == psy1i) {
-                // same source scan line as just computed
-                goto copy;
-            }
-
-            psy0i = sy0i;
-            psy1i = sy1i;
-
-            for (int x = 0; x < w; ++x) {
-                peaks[x] = 0;
-            }
-        
-            for (int sy = sy0i; sy <= sy1i; ++sy) {
-
-                if (sy < 0 || sy >= source->height()) continue;
-
-                uchar *sourceLine = source->scanLine(sy);
-            
-                for (int x = 0; x < w; ++x) {
-
-                    double sx1 = sxa[x*2 + 1];
-                    if (sx1 < 0) continue;
-                    int sx1i = int(sx1);
-
-                    double sx0 = sxa[x*2];
-                    if (sx0 < 0) continue;
-                    int sx0i = int(sx0 + epsilon);
-                    if (sx0i >= sw) break;
-
-                    uchar peak = 0;
-                    for (int sx = sx0i; sx <= sx1i; ++sx) {
-                        if (sx < 0 || sx >= sw) continue;
-                        if (sourceLine[sx] > peak) peak = sourceLine[sx];
-                    }
-                    peaks[x] = peak;
-                }
-            }
-        
-        copy:
-            for (int x = 0; x < w; ++x) {
-                targetLine[x] = peaks[x];
-            }
-        }
-    }
-
-    delete[] peaks;
-
-    paint.drawImage(x0, 0, img);
+    paintWithRenderer(v, paint, rect);
 }
 
 bool
@@ -1726,25 +1185,40 @@
 {
     QString s = QString("scale=\"%1\" "
                         "colourScheme=\"%2\" "
-                        "normalizeColumns=\"%3\" "
-                        "normalizeVisibleArea=\"%4\" "
-                        "minY=\"%5\" "
-                        "maxY=\"%6\" "
-                        "invertVertical=\"%7\" "
-                        "opaque=\"%8\" %9")
-	.arg((int)m_colourScale)
+                        "minY=\"%3\" "
+                        "maxY=\"%4\" "
+                        "invertVertical=\"%5\" "
+                        "opaque=\"%6\" %7")
+	.arg(convertFromColourScale(m_colourScale))
         .arg(m_colourMap)
-        .arg(m_normalizeColumns ? "true" : "false")
-        .arg(m_normalizeVisibleArea ? "true" : "false")
         .arg(m_miny)
         .arg(m_maxy)
         .arg(m_invertVertical ? "true" : "false")
         .arg(m_opaque ? "true" : "false")
         .arg(QString("binScale=\"%1\" smooth=\"%2\" gain=\"%3\" ")
-             .arg((int)m_binScale)
+             .arg(int(m_binScale))
              .arg(m_smooth ? "true" : "false")
              .arg(m_gain));
     
+    // New-style normalization attributes, allowing for more types of
+    // normalization in future: write out the column normalization
+    // type separately, and then whether we are normalizing visible
+    // area as well afterwards
+    
+    s += QString("columnNormalization=\"%1\" ")
+        .arg(m_normalization == ColumnNormalization::Max1 ? "peak" :
+             m_normalization == ColumnNormalization::Hybrid ? "hybrid" : "none");
+
+    // Old-style normalization attribute, for backward compatibility
+    
+    s += QString("normalizeColumns=\"%1\" ")
+	.arg(m_normalization == ColumnNormalization::Max1 ? "true" : "false");
+
+    // And this applies to both old- and new-style attributes
+    
+    s += QString("normalizeVisibleArea=\"%1\" ")
+        .arg(m_normalizeVisibleArea ? "true" : "false");
+    
     Layer::toXml(stream, indent, extraAttributes + " " + s);
 }
 
@@ -1753,22 +1227,16 @@
 {
     bool ok = false, alsoOk = false;
 
-    ColourScale scale = (ColourScale)attributes.value("scale").toInt(&ok);
-    if (ok) setColourScale(scale);
+    ColourScaleType colourScale = convertToColourScale
+        (attributes.value("colourScale").toInt(&ok));
+    if (ok) setColourScale(colourScale);
 
     int colourMap = attributes.value("colourScheme").toInt(&ok);
     if (ok) setColourMap(colourMap);
 
-    BinScale binscale = (BinScale)attributes.value("binScale").toInt(&ok);
-    if (ok) setBinScale(binscale);
-
-    bool normalizeColumns =
-        (attributes.value("normalizeColumns").trimmed() == "true");
-    setNormalizeColumns(normalizeColumns);
-
-    bool normalizeVisibleArea =
-        (attributes.value("normalizeVisibleArea").trimmed() == "true");
-    setNormalizeVisibleArea(normalizeVisibleArea);
+    BinScale binScale = (BinScale)
+	attributes.value("binScale").toInt(&ok);
+    if (ok) setBinScale(binScale);
 
     bool invertVertical =
         (attributes.value("invertVertical").trimmed() == "true");
@@ -1788,5 +1256,50 @@
     float min = attributes.value("minY").toFloat(&ok);
     float max = attributes.value("maxY").toFloat(&alsoOk);
     if (ok && alsoOk) setDisplayExtents(min, max);
+
+    bool haveNewStyleNormalization = false;
+    
+    QString columnNormalization = attributes.value("columnNormalization");
+
+    if (columnNormalization != "") {
+
+        haveNewStyleNormalization = true;
+
+        if (columnNormalization == "peak") {
+            setNormalization(ColumnNormalization::Max1);
+        } else if (columnNormalization == "hybrid") {
+            setNormalization(ColumnNormalization::Hybrid);
+        } else if (columnNormalization == "none") {
+            setNormalization(ColumnNormalization::None);
+        } else {
+            cerr << "NOTE: Unknown or unsupported columnNormalization attribute \""
+                 << columnNormalization << "\"" << endl;
+        }
+    }
+
+    if (!haveNewStyleNormalization) {
+
+        setNormalization(ColumnNormalization::None);
+
+        bool normalizeColumns =
+            (attributes.value("normalizeColumns").trimmed() == "true");
+        if (normalizeColumns) {
+            setNormalization(ColumnNormalization::Max1);
+        }
+
+        bool normalizeHybrid =
+            (attributes.value("normalizeHybrid").trimmed() == "true");
+        if (normalizeHybrid) {
+            setNormalization(ColumnNormalization::Hybrid);
+        }
+    }
+    
+    bool normalizeVisibleArea =
+        (attributes.value("normalizeVisibleArea").trimmed() == "true");
+    setNormalizeVisibleArea(normalizeVisibleArea);
+
+    //!!! todo: check save/reload scaling, compare with
+    //!!! SpectrogramLayer, compare with prior SV versions, compare
+    //!!! with Tony v1 and v2 and their save files
 }
 
--- a/layer/Colour3DPlotLayer.h	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/Colour3DPlotLayer.h	Fri Aug 19 15:58:57 2016 +0100
@@ -13,10 +13,14 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _COLOUR_3D_PLOT_H_
-#define _COLOUR_3D_PLOT_H_
+#ifndef COLOUR_3D_PLOT_LAYER_H
+#define COLOUR_3D_PLOT_LAYER_H
 
 #include "SliceableLayer.h"
+#include "VerticalBinLayer.h"
+
+#include "ColourScale.h"
+#include "Colour3DPlotRenderer.h"
 
 #include "data/model/DenseThreeDimensionalModel.h"
 
@@ -30,14 +34,11 @@
  * colour range.  Its source is a DenseThreeDimensionalModel.
  *
  * This was the original implementation for the spectrogram view, but
- * it was replaced with a more efficient implementation that derived
- * the spectrogram itself from a DenseTimeValueModel instead of using
- * a three-dimensional model.  This class is retained in case it
- * becomes useful, but it will probably need some cleaning up if it's
- * ever actually used.
+ * it was replaced for that purpose with a more efficient
+ * implementation that derived the spectrogram itself from a
+ * DenseTimeValueModel instead of using a three-dimensional model.
  */
-
-class Colour3DPlotLayer : public SliceableLayer
+class Colour3DPlotLayer : public VerticalBinLayer
 {
     Q_OBJECT
 
@@ -50,6 +51,7 @@
     }
     virtual const Model *getModel() const { return m_model; }
     virtual void paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
+    virtual void setSynchronousPainting(bool synchronous);
 
     virtual int getVerticalScaleWidth(LayerGeometryProvider *v, bool, QPainter &) const;
     virtual void paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect rect) const;
@@ -81,19 +83,14 @@
                                          int *min, int *max, int *deflt) const;
     virtual QString getPropertyValueLabel(const PropertyName &,
 					  int value) const;
+    virtual QString getPropertyValueIconName(const PropertyName &,
+                                             int value) const;
     virtual RangeMapper *getNewPropertyRangeMapper(const PropertyName &) const;
     virtual void setProperty(const PropertyName &, int value);
     virtual void setProperties(const QXmlAttributes &);
     
-    enum ColourScale {
-        LinearScale,
-        LogScale,
-        PlusMinusOneScale,
-        AbsoluteScale
-    };
-
-    void setColourScale(ColourScale);
-    ColourScale getColourScale() const { return m_colourScale; }
+    void setColourScale(ColourScaleType);
+    ColourScaleType getColourScale() const { return m_colourScale; }
 
     void setColourMap(int map);
     int getColourMap() const;
@@ -104,11 +101,6 @@
      */
     void setGain(float gain);
     float getGain() const;
-
-    enum BinScale {
-	LinearBinScale,
-	LogBinScale
-    };
     
     /**
      * Specify the scale for the y axis.
@@ -117,31 +109,20 @@
     BinScale getBinScale() const;
 
     /**
-     * Normalize each column to its maximum value, independent of its
-     * neighbours.
+     * Specify the normalization mode for individual columns.
      */
-    void setNormalizeColumns(bool n);
-    bool getNormalizeColumns() const;
+    void setNormalization(ColumnNormalization);
+    ColumnNormalization getNormalization() const;
 
     /**
-     * Normalize each value against the maximum in the visible region.
+     * Specify whether to normalize the visible area.
      */
-    void setNormalizeVisibleArea(bool n);
+    void setNormalizeVisibleArea(bool);
     bool getNormalizeVisibleArea() const;
 
-    /**
-     * Normalize each column to its maximum value, and then scale by
-     * the log of the (absolute) maximum value.
-     */
-    void setNormalizeHybrid(bool n);
-    bool getNormalizeHybrid() const;
-
     void setInvertVertical(bool i);
     bool getInvertVertical() const;
 
-    void setShowRectified(bool);
-    bool getShowRectified() const { return m_rectified; }
-
     void setOpaque(bool i);
     bool getOpaque() const;
 
@@ -176,42 +157,50 @@
 protected:
     const DenseThreeDimensionalModel *m_model; // I do not own this
     
-    mutable QImage *m_cache;
-    mutable QImage *m_peaksCache;
-    mutable int m_cacheValidStart;
-    mutable int m_cacheValidEnd;
-
-    ColourScale m_colourScale;
-    bool        m_colourScaleSet;
-    int         m_colourMap;
-    float       m_gain;
-    BinScale    m_binScale;
-    bool        m_normalizeColumns;
-    bool        m_normalizeVisibleArea;
-    bool        m_normalizeHybrid;
-    bool        m_invertVertical;
-    bool        m_rectified;
-    bool        m_opaque;
-    bool        m_smooth;
-    int         m_peakResolution;
+    ColourScaleType m_colourScale;
+    bool m_colourScaleSet;
+    int m_colourMap;
+    float m_gain;
+    BinScale m_binScale;
+    ColumnNormalization m_normalization; // of individual columns
+    bool m_normalizeVisibleArea;
+    bool m_invertVertical;
+    bool m_opaque;
+    bool m_smooth;
+    int m_peakResolution;
 
     // Minimum and maximum bin numbers visible within the view. We
     // always snap to whole bins at view edges.
-    int         m_miny;
-    int         m_maxy;
+    int m_miny;
+    int m_maxy;
 
+    bool m_synchronous;
+
+    static ColourScaleType convertToColourScale(int value);
+    static int convertFromColourScale(ColourScaleType);
+    static std::pair<ColumnNormalization, bool> convertToColumnNorm(int value);
+    static int convertFromColumnNorm(ColumnNormalization norm, bool visible);
+
+    mutable Dense3DModelPeakCache *m_peakCache;
+    const int m_peakCacheDivisor;
+    Dense3DModelPeakCache *getPeakCache() const;
+
+    typedef std::map<int, MagnitudeRange> ViewMagMap; // key is view id
+    mutable ViewMagMap m_viewMags;
+
+    typedef std::map<int, Colour3DPlotRenderer *> ViewRendererMap; // key is view id
+    mutable ViewRendererMap m_renderers;
+    
+    Colour3DPlotRenderer *getRenderer(const LayerGeometryProvider *) const;
+    void invalidateRenderers();
+        
     /**
      * Return the y coordinate at which the given bin "starts"
      * (i.e. at the bottom of the bin, if the given bin is an integer
      * and the vertical scale is the usual way up). Bin number may be
      * fractional, to obtain a position part-way through a bin.
      */
-    double getYForBin(LayerGeometryProvider *, double bin) const;
-
-    /**
-     * As getYForBin, but rounding to integer values.
-     */
-    int getIYForBin(LayerGeometryProvider *, int bin) const;
+    double getYForBin(const LayerGeometryProvider *, double bin) const;
     
     /**
      * Return the bin number, possibly fractional, at the given y
@@ -219,25 +208,13 @@
      * at which the bins "start" (i.e. the bottom of the visible bin,
      * if the vertical scale is the usual way up).
      */
-    double getBinForY(LayerGeometryProvider *, double y) const;
-
-    /**
-     * As getBinForY, but rounding to integer values.
-     */
-    int getIBinForY(LayerGeometryProvider *, int y) const;
+    double getBinForY(const LayerGeometryProvider *, double y) const;
     
     DenseThreeDimensionalModel::Column getColumn(int col) const;
 
-    /**
-     * True if we have the opaque or smooth flag set, or if the cells
-     * are so small you can't see their borders. False for big,
-     * translucent cells.
-     */
-    bool shouldPaintDenseIn(const LayerGeometryProvider *) const; 
+    int getColourScaleWidth(QPainter &) const;
 
-    int getColourScaleWidth(QPainter &) const;
-    void fillCache(int firstBin, int lastBin) const;
-    void paintDense(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
+    void paintWithRenderer(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
 };
 
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/Colour3DPlotRenderer.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -0,0 +1,1051 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "Colour3DPlotRenderer.h"
+#include "RenderTimer.h"
+
+#include "base/Profiler.h"
+
+#include "data/model/DenseThreeDimensionalModel.h"
+#include "data/model/Dense3DModelPeakCache.h"
+#include "data/model/FFTModel.h"
+
+#include "LayerGeometryProvider.h"
+#include "VerticalBinLayer.h"
+#include "PaintAssistant.h"
+#include "ImageRegionFinder.h"
+
+#include "view/ViewManager.h" // for main model sample rate. Pity
+
+#include <vector>
+
+//#define DEBUG_COLOUR_PLOT_REPAINT 1
+
+using namespace std;
+
+Colour3DPlotRenderer::RenderResult
+Colour3DPlotRenderer::render(const LayerGeometryProvider *v, QPainter &paint, QRect rect)
+{
+    return render(v, paint, rect, false);
+}
+
+Colour3DPlotRenderer::RenderResult
+Colour3DPlotRenderer::renderTimeConstrained(const LayerGeometryProvider *v,
+                                            QPainter &paint, QRect rect)
+{
+    return render(v, paint, rect, true);
+}
+
+QRect
+Colour3DPlotRenderer::getLargestUncachedRect(const LayerGeometryProvider *v)
+{
+    RenderType renderType = decideRenderType(v);
+
+    if (renderType == DirectTranslucent) {
+        return QRect(); // never cached
+    }
+
+    int h = m_cache.getSize().height();
+
+    QRect areaLeft(0, 0, m_cache.getValidLeft(), h);
+    QRect areaRight(m_cache.getValidRight(), 0,
+                    m_cache.getSize().width() - m_cache.getValidRight(), h);
+
+    if (areaRight.width() > areaLeft.width()) {
+        return areaRight;
+    } else {
+        return areaLeft;
+    }
+}
+
+bool
+Colour3DPlotRenderer::geometryChanged(const LayerGeometryProvider *v)
+{
+    RenderType renderType = decideRenderType(v);
+
+    if (renderType == DirectTranslucent) {
+        return true; // never cached
+    }
+
+    if (m_cache.getSize() == v->getPaintSize() &&
+        m_cache.getZoomLevel() == v->getZoomLevel() &&
+        m_cache.getStartFrame() == v->getStartFrame()) {
+        return false;
+    } else {
+        return true;
+    }
+}
+
+Colour3DPlotRenderer::RenderResult
+Colour3DPlotRenderer::render(const LayerGeometryProvider *v,
+                             QPainter &paint, QRect rect, bool timeConstrained)
+{
+    RenderType renderType = decideRenderType(v);
+
+    if (renderType != DrawBufferPixelResolution) {
+        // Rendering should be fast in bin-resolution and direct draw
+        // cases because we are quite well zoomed-in, and the sums are
+        // easier this way. Calculating boundaries later will be
+        // fiddly for partial paints otherwise.
+        timeConstrained = false;
+    }
+
+    int x0 = v->getXForViewX(rect.x());
+    int x1 = v->getXForViewX(rect.x() + rect.width());
+    if (x0 < 0) x0 = 0;
+    if (x1 > v->getPaintWidth()) x1 = v->getPaintWidth();
+
+    sv_frame_t startFrame = v->getStartFrame();
+    
+    m_cache.resize(v->getPaintSize());
+    m_cache.setZoomLevel(v->getZoomLevel());
+
+    m_magCache.resize(v->getPaintSize().width());
+    m_magCache.setZoomLevel(v->getZoomLevel());
+    
+    if (renderType == DirectTranslucent) {
+        MagnitudeRange range = renderDirectTranslucent(v, paint, rect);
+        return { rect, range };
+    }
+    
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    cerr << "cache start " << m_cache.getStartFrame()
+         << " valid left " << m_cache.getValidLeft()
+         << " valid right " << m_cache.getValidRight()
+         << endl;
+    cerr << " view start " << startFrame
+         << " x0 " << x0
+         << " x1 " << x1
+         << endl;
+#endif
+    
+    if (m_cache.isValid()) { // some part of the cache is valid
+
+        if (v->getXForFrame(m_cache.getStartFrame()) ==
+            v->getXForFrame(startFrame) &&
+            m_cache.getValidLeft() <= x0 &&
+            m_cache.getValidRight() >= x1) {
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+            cerr << "cache hit" << endl;
+#endif
+            
+            // cache is valid for the complete requested area
+            paint.drawImage(rect, m_cache.getImage(), rect);
+
+            MagnitudeRange range = m_magCache.getRange(x0, x1 - x0);
+
+            return { rect, range };
+
+        } else {
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+            cerr << "cache partial hit" << endl;
+#endif
+            
+            // cache doesn't begin at the right frame or doesn't
+            // contain the complete view, but might be scrollable or
+            // partially usable
+            m_cache.scrollTo(v, startFrame);
+            m_magCache.scrollTo(v, startFrame);
+
+            // if we are not time-constrained, then we want to paint
+            // the whole area in one go; we don't return a partial
+            // paint. To avoid providing the more complex logic to
+            // handle painting discontiguous areas, if the only valid
+            // part of cache is in the middle, just make the whole
+            // thing invalid and start again.
+            if (!timeConstrained) {
+                if (m_cache.getValidLeft() > x0 &&
+                    m_cache.getValidRight() < x1) {
+                    m_cache.invalidate();
+                }
+            }
+        }
+    } else {
+        // cache is completely invalid
+        m_cache.setStartFrame(startFrame);
+        m_magCache.setStartFrame(startFrame);
+    }
+
+    bool rightToLeft = false;
+
+    int reqx0 = x0;
+    int reqx1 = x1;
+    
+    if (!m_cache.isValid() && timeConstrained) {
+        // When rendering the whole area, in a context where we might
+        // not be able to complete the work, start from somewhere near
+        // the middle so that the region of interest appears first
+
+        //!!! (perhaps we should avoid doing this if past repaints
+        //!!! have been fast enough to do the whole in one shot)
+        if (x0 == 0 && x1 == v->getPaintWidth()) {
+            x0 = int(x1 * 0.3);
+        }
+    }
+
+    if (m_cache.isValid()) {
+            
+        // When rendering only a part of the cache, we need to make
+        // sure that the part we're rendering is adjacent to (or
+        // overlapping) a valid area of cache, if we have one. The
+        // alternative is to ditch the valid area of cache and render
+        // only the requested area, but that's risky because this can
+        // happen when just waving the pointer over a small part of
+        // the view -- if we lose the partly-built cache every time
+        // the user does that, we'll never finish building it.
+        int left = x0;
+        int width = x1 - x0;
+        bool isLeftOfValidArea = false;
+        m_cache.adjustToTouchValidArea(left, width, isLeftOfValidArea);
+        x0 = left;
+        x1 = x0 + width;
+
+        // That call also told us whether we should be painting
+        // sub-regions of our target region in right-to-left order in
+        // order to ensure contiguity
+        rightToLeft = isLeftOfValidArea;
+    }
+    
+    // Note, we always paint the full height to cache. We want to
+    // ensure the cache is coherent without having to worry about
+    // vertical matching of required and valid areas as well as
+    // horizontal.
+
+    if (renderType == DrawBufferBinResolution) {
+
+        renderToCacheBinResolution(v, x0, x1 - x0);
+
+    } else { // must be DrawBufferPixelResolution, handled DirectTranslucent earlier
+
+        renderToCachePixelResolution(v, x0, x1 - x0, rightToLeft, timeConstrained);
+    }
+
+    QRect pr = rect & m_cache.getValidArea();
+    paint.drawImage(pr.x(), pr.y(), m_cache.getImage(),
+                    pr.x(), pr.y(), pr.width(), pr.height());
+
+    if (!timeConstrained && (pr != rect)) {
+        cerr << "WARNING: failed to render entire requested rect "
+             << "even when not time-constrained" << endl;
+    }
+
+    MagnitudeRange range = m_magCache.getRange(reqx0, reqx1 - reqx0);
+    
+    return { pr, range };
+}
+
+Colour3DPlotRenderer::RenderType
+Colour3DPlotRenderer::decideRenderType(const LayerGeometryProvider *v) const
+{
+    const DenseThreeDimensionalModel *model = m_sources.source;
+    if (!model || !v || !(v->getViewManager())) {
+        return DrawBufferPixelResolution; // or anything
+    }
+
+    int binResolution = model->getResolution();
+    int zoomLevel = v->getZoomLevel();
+    sv_samplerate_t modelRate = model->getSampleRate();
+
+    double rateRatio = v->getViewManager()->getMainModelSampleRate() / modelRate;
+    double relativeBinResolution = binResolution * rateRatio;
+
+    if (m_params.binDisplay == BinDisplay::PeakFrequencies) {
+        // no alternative works here
+        return DrawBufferPixelResolution;
+    }
+
+    if (!m_params.alwaysOpaque && !m_params.interpolate) {
+
+        // consider translucent option -- only if not smoothing & not
+        // explicitly requested opaque & sufficiently zoomed-in
+        
+        if (model->getHeight() * 3 < v->getPaintHeight() &&
+            relativeBinResolution >= 3 * zoomLevel) {
+            return DirectTranslucent;
+        }
+    }
+
+    if (relativeBinResolution > zoomLevel) {
+        return DrawBufferBinResolution;
+    } else {
+        return DrawBufferPixelResolution;
+    }
+}
+
+ColumnOp::Column
+Colour3DPlotRenderer::getColumn(int sx, int minbin, int nbins) const
+{
+    // order:
+    // get column -> scale -> normalise -> record extents ->
+    // peak pick -> distribute/interpolate -> apply display gain
+
+    // we do the first bit here:
+    // get column -> scale -> normalise
+
+    ColumnOp::Column column;
+                
+    if (m_params.colourScale.getScale() == ColourScaleType::Phase &&
+        m_sources.fft) {
+
+        ColumnOp::Column fullColumn = m_sources.fft->getPhases(sx);
+
+        column = vector<float>(fullColumn.data() + minbin,
+                               fullColumn.data() + minbin + nbins);
+
+    } else {
+                    
+        ColumnOp::Column fullColumn = m_sources.source->getColumn(sx);
+                
+        column = vector<float>(fullColumn.data() + minbin,
+                               fullColumn.data() + minbin + nbins);
+
+        column = ColumnOp::applyGain(column, m_params.scaleFactor);
+
+        column = ColumnOp::normalize(column, m_params.normalization);
+    }
+
+    return column;
+}
+
+MagnitudeRange
+Colour3DPlotRenderer::renderDirectTranslucent(const LayerGeometryProvider *v,
+                                              QPainter &paint,
+                                              QRect rect)
+{
+    Profiler profiler("Colour3DPlotRenderer::renderDirectTranslucent");
+
+    MagnitudeRange magRange;
+    
+    QPoint illuminatePos;
+    bool illuminate = v->shouldIlluminateLocalFeatures
+        (m_sources.verticalBinLayer, illuminatePos);
+
+    const DenseThreeDimensionalModel *model = m_sources.source;
+    
+    int x0 = rect.left();
+    int x1 = rect.right() + 1;
+
+    int h = v->getPaintHeight();
+
+    sv_frame_t modelStart = model->getStartFrame();
+    sv_frame_t modelEnd = model->getEndFrame();
+    int modelResolution = model->getResolution();
+
+    double rateRatio =
+        v->getViewManager()->getMainModelSampleRate() / model->getSampleRate();
+
+    // the s-prefix values are source, i.e. model, column and bin numbers
+    int sx0 = int((double(v->getFrameForX(x0)) / rateRatio - double(modelStart))
+                  / modelResolution);
+    int sx1 = int((double(v->getFrameForX(x1)) / rateRatio - double(modelStart))
+                  / modelResolution);
+
+    int sh = model->getHeight();
+
+    const int buflen = 40;
+    char labelbuf[buflen];
+
+    int minbin = m_sources.verticalBinLayer->getIBinForY(v, h);
+    if (minbin >= sh) minbin = sh - 1;
+    if (minbin < 0) minbin = 0;
+    
+    int nbins  = m_sources.verticalBinLayer->getIBinForY(v, 0) - minbin + 1;
+    if (minbin + nbins > sh) nbins = sh - minbin;
+
+    int psx = -1;
+
+    vector<float> preparedColumn;
+
+    int modelWidth = model->getWidth();
+
+    for (int sx = sx0; sx <= sx1; ++sx) {
+
+        if (sx < 0 || sx >= modelWidth) {
+            continue;
+        }
+
+        if (sx != psx) {
+
+            // order:
+            // get column -> scale -> normalise -> record extents ->
+            // peak pick -> distribute/interpolate -> apply display gain
+
+            // this does the first three:
+            preparedColumn = getColumn(sx, minbin, nbins);
+            
+            magRange.sample(preparedColumn);
+
+            if (m_params.binDisplay == BinDisplay::PeakBins) {
+                preparedColumn = ColumnOp::peakPick(preparedColumn);
+            }
+
+            // Display gain belongs to the colour scale and is
+            // applied by the colour scale object when mapping it
+
+            psx = sx;
+        }
+
+	sv_frame_t fx = sx * modelResolution + modelStart;
+
+	if (fx + modelResolution <= modelStart || fx > modelEnd) continue;
+
+        int rx0 = v->getXForFrame(int(double(fx) * rateRatio));
+	int rx1 = v->getXForFrame(int(double(fx + modelResolution + 1) * rateRatio));
+
+	int rw = rx1 - rx0;
+	if (rw < 1) rw = 1;
+
+	bool showLabel = (rw > 10 &&
+			  paint.fontMetrics().width("0.000000") < rw - 3 &&
+			  paint.fontMetrics().height() < (h / sh));
+        
+	for (int sy = minbin; sy < minbin + nbins; ++sy) {
+
+            int ry0 = m_sources.verticalBinLayer->getIYForBin(v, sy);
+            int ry1 = m_sources.verticalBinLayer->getIYForBin(v, sy + 1);
+
+            if (m_params.invertVertical) {
+                ry0 = h - ry0 - 1;
+                ry1 = h - ry1 - 1;
+            }
+                    
+            QRect r(rx0, ry1, rw, ry0 - ry1);
+
+            float value = preparedColumn[sy - minbin];
+            QColor colour = m_params.colourScale.getColour(value,
+                                                           m_params.colourRotation);
+
+            if (rw == 1) {
+                paint.setPen(colour);
+                paint.setBrush(Qt::NoBrush);
+                paint.drawLine(r.x(), r.y(), r.x(), r.y() + r.height() - 1);
+                continue;
+            }
+
+	    QColor pen(255, 255, 255, 80);
+	    QColor brush(colour);
+
+            if (rw > 3 && r.height() > 3) {
+                brush.setAlpha(160);
+            }
+
+	    paint.setPen(Qt::NoPen);
+	    paint.setBrush(brush);
+
+	    if (illuminate) {
+		if (r.contains(illuminatePos)) {
+		    paint.setPen(v->getForeground());
+		}
+	    }
+            
+#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
+//            cerr << "rect " << r.x() << "," << r.y() << " "
+//                      << r.width() << "x" << r.height() << endl;
+#endif
+
+	    paint.drawRect(r);
+
+	    if (showLabel) {
+                double value = model->getValueAt(sx, sy);
+                snprintf(labelbuf, buflen, "%06f", value);
+                QString text(labelbuf);
+                PaintAssistant::drawVisibleText
+                    (v,
+                     paint,
+                     rx0 + 2,
+                     ry0 - h / sh - 1 + 2 + paint.fontMetrics().ascent(),
+                     text,
+                     PaintAssistant::OutlinedText);
+	    }
+	}
+    }
+
+    return magRange;
+}
+
+void
+Colour3DPlotRenderer::renderToCachePixelResolution(const LayerGeometryProvider *v,
+                                                   int x0, int repaintWidth,
+                                                   bool rightToLeft,
+                                                   bool timeConstrained)
+{
+    Profiler profiler("Colour3DPlotRenderer::renderToCachePixelResolution");
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    cerr << "renderToCachePixelResolution" << endl;
+#endif
+    
+    // Draw to the draw buffer, and then copy from there. The draw
+    // buffer is at the same resolution as the target in the cache, so
+    // no extra scaling needed.
+
+    const DenseThreeDimensionalModel *model = m_sources.source;
+    if (!model || !model->isOK() || !model->isReady()) {
+	throw std::logic_error("no source model provided, or model not ready");
+    }
+
+    int h = v->getPaintHeight();
+
+    clearDrawBuffer(repaintWidth, h);
+
+    vector<int> binforx(repaintWidth);
+    vector<double> binfory(h);
+    
+    bool usePeaksCache = false;
+    int binsPerPeak = 1;
+    int zoomLevel = v->getZoomLevel();
+    int binResolution = model->getResolution();
+
+    for (int x = 0; x < repaintWidth; ++x) {
+        sv_frame_t f0 = v->getFrameForX(x0 + x);
+        double s0 = double(f0 - model->getStartFrame()) / binResolution;
+        binforx[x] = int(s0 + 0.0001);
+    }
+
+    if (m_sources.peaks) { // peaks cache exists
+
+        binsPerPeak = m_sources.peaks->getColumnsPerPeak();
+        usePeaksCache = (binResolution * binsPerPeak) < zoomLevel;
+        
+        if (m_params.colourScale.getScale() ==
+            ColourScaleType::Phase) {
+            usePeaksCache = false;
+        }
+    }
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    cerr << "[PIX] zoomLevel = " << zoomLevel
+         << ", binResolution " << binResolution 
+         << ", binsPerPeak " << binsPerPeak
+         << ", peak cache " << m_sources.peaks
+         << ", usePeaksCache = " << usePeaksCache
+         << endl;
+#endif
+    
+    for (int y = 0; y < h; ++y) {
+        binfory[y] = m_sources.verticalBinLayer->getBinForY(v, h - y - 1);
+    }
+
+    int attainedWidth;
+
+    if (m_params.binDisplay == BinDisplay::PeakFrequencies) {
+        attainedWidth = renderDrawBufferPeakFrequencies(v,
+                                                        repaintWidth,
+                                                        h,
+                                                        binforx,
+                                                        binfory,
+                                                        rightToLeft,
+                                                        timeConstrained);
+
+    } else {
+        attainedWidth = renderDrawBuffer(repaintWidth,
+                                         h,
+                                         binforx,
+                                         binfory,
+                                         usePeaksCache,
+                                         rightToLeft,
+                                         timeConstrained);
+    }
+
+    if (attainedWidth == 0) return;
+
+    // draw buffer is pixel resolution, no scaling factors or padding involved
+    
+    int paintedLeft = x0;
+    if (rightToLeft) {
+        paintedLeft += (repaintWidth - attainedWidth);
+    }
+
+    m_cache.drawImage(paintedLeft, attainedWidth,
+                      m_drawBuffer,
+                      paintedLeft - x0, attainedWidth);
+
+    for (int i = 0; in_range_for(m_magRanges, i); ++i) {
+        m_magCache.sampleColumn(i, m_magRanges.at(i));
+    }
+}
+
+void
+Colour3DPlotRenderer::renderToCacheBinResolution(const LayerGeometryProvider *v,
+                                                 int x0, int repaintWidth)
+{
+    Profiler profiler("Colour3DPlotRenderer::renderToCacheBinResolution");
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    cerr << "renderToCacheBinResolution" << endl;
+#endif
+    
+    // Draw to the draw buffer, and then scale-copy from there. Draw
+    // buffer is at bin resolution, i.e. buffer x == source column
+    // number. We use toolkit smooth scaling for interpolation.
+
+    const DenseThreeDimensionalModel *model = m_sources.source;
+    if (!model || !model->isOK() || !model->isReady()) {
+	throw std::logic_error("no source model provided, or model not ready");
+    }
+
+    // The draw buffer will contain a fragment at bin resolution. We
+    // need to ensure that it starts and ends at points where a
+    // time-bin boundary occurs at an exact pixel boundary, and with a
+    // certain amount of overlap across existing pixels so that we can
+    // scale and draw from it without smoothing errors at the edges.
+
+    // If (getFrameForX(x) / increment) * increment ==
+    // getFrameForX(x), then x is a time-bin boundary.  We want two
+    // such boundaries at either side of the draw buffer -- one which
+    // we draw up to, and one which we subsequently crop at.
+
+    sv_frame_t leftBoundaryFrame = -1, leftCropFrame = -1;
+    sv_frame_t rightBoundaryFrame = -1, rightCropFrame = -1;
+
+    int drawBufferWidth;
+    int binResolution = model->getResolution();
+
+    for (int x = x0; ; --x) {
+        sv_frame_t f = v->getFrameForX(x);
+        if ((f / binResolution) * binResolution == f) {
+            if (leftCropFrame == -1) leftCropFrame = f;
+            else if (x < x0 - 2) {
+                leftBoundaryFrame = f;
+                break;
+            }
+        }
+    }
+    for (int x = x0 + repaintWidth; ; ++x) {
+        sv_frame_t f = v->getFrameForX(x);
+        if ((f / binResolution) * binResolution == f) {
+            if (rightCropFrame == -1) rightCropFrame = f;
+            else if (x > x0 + repaintWidth + 2) {
+                rightBoundaryFrame = f;
+                break;
+            }
+        }
+    }
+    drawBufferWidth = int
+        ((rightBoundaryFrame - leftBoundaryFrame) / binResolution);
+    
+    int h = v->getPaintHeight();
+
+    // For our purposes here, the draw buffer needs to be exactly our
+    // target size (so we recreate always rather than just clear it)
+    
+    recreateDrawBuffer(drawBufferWidth, h);
+
+    vector<int> binforx(drawBufferWidth);
+    vector<double> binfory(h);
+    
+    for (int x = 0; x < drawBufferWidth; ++x) {
+        binforx[x] = int(leftBoundaryFrame / binResolution) + x;
+    }
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    cerr << "[BIN] binResolution " << binResolution 
+         << endl;
+#endif
+    
+    for (int y = 0; y < h; ++y) {
+        binfory[y] = m_sources.verticalBinLayer->getBinForY(v, h - y - 1);
+    }
+
+    int attainedWidth = renderDrawBuffer(drawBufferWidth,
+                                         h,
+                                         binforx,
+                                         binfory,
+                                         false,
+                                         false,
+                                         false);
+
+    if (attainedWidth == 0) return;
+
+    int scaledLeft = v->getXForFrame(leftBoundaryFrame);
+    int scaledRight = v->getXForFrame(rightBoundaryFrame);
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    cerr << "scaling draw buffer from width " << m_drawBuffer.width()
+         << " to " << (scaledRight - scaledLeft) << " (nb drawBufferWidth = "
+         << drawBufferWidth << ")" << endl;
+#endif
+    
+    QImage scaled = m_drawBuffer.scaled
+        (scaledRight - scaledLeft, h,
+         Qt::IgnoreAspectRatio, (m_params.interpolate ?
+                                 Qt::SmoothTransformation :
+                                 Qt::FastTransformation));
+            
+    int scaledLeftCrop = v->getXForFrame(leftCropFrame);
+    int scaledRightCrop = v->getXForFrame(rightCropFrame);
+    
+    int targetLeft = scaledLeftCrop;
+    if (targetLeft < 0) {
+        targetLeft = 0;
+    }
+    
+    int targetWidth = scaledRightCrop - targetLeft;
+    if (targetLeft + targetWidth > m_cache.getSize().width()) {
+        targetWidth = m_cache.getSize().width() - targetLeft;
+    }
+    
+    int sourceLeft = targetLeft - scaledLeft;
+    if (sourceLeft < 0) {
+        sourceLeft = 0;
+    }
+    
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    cerr << "repaintWidth = " << repaintWidth
+         << ", targetWidth = " << targetWidth << endl;
+#endif
+    
+    if (targetWidth > 0) {
+        // we are copying from an image that has already been scaled,
+        // hence using the same width in both geometries
+        m_cache.drawImage(targetLeft, targetWidth,
+                          scaled,
+                          sourceLeft, targetWidth);
+    }
+    
+    for (int i = 0; i < targetWidth; ++i) {
+        // but the mag range vector has not been scaled
+        int sourceIx = int((double(i + sourceLeft) / scaled.width())
+                           * int(m_magRanges.size()));
+        if (in_range_for(m_magRanges, sourceIx)) {
+            m_magCache.sampleColumn(i, m_magRanges.at(sourceIx));
+        }
+    }
+}
+
+int
+Colour3DPlotRenderer::renderDrawBuffer(int w, int h,
+                                       const vector<int> &binforx,
+                                       const vector<double> &binfory,
+                                       bool usePeaksCache,
+                                       bool rightToLeft,
+                                       bool timeConstrained)
+{
+    // Callers must have checked that the appropriate subset of
+    // Sources data members are set for the supplied flags (e.g. that
+    // peaks model exists if usePeaksCache)
+    
+    RenderTimer timer(timeConstrained ?
+                      RenderTimer::FastRender :
+                      RenderTimer::NoTimeout);
+
+    int divisor = 1;
+    const DenseThreeDimensionalModel *sourceModel = m_sources.source;
+    if (usePeaksCache) {
+        divisor = m_sources.peaks->getColumnsPerPeak();
+        sourceModel = m_sources.peaks;
+    }
+
+    int sh = sourceModel->getHeight();
+    
+    int minbin = int(binfory[0] + 0.0001);
+    if (minbin >= sh) minbin = sh - 1;
+    if (minbin < 0) minbin = 0;
+
+    int nbins  = int(binfory[h-1]) - minbin + 1;
+    if (minbin + nbins > sh) nbins = sh - minbin;
+    
+    int psx = -1;
+
+    int start = 0;
+    int finish = w;
+    int step = 1;
+
+    if (rightToLeft) {
+        start = w-1;
+        finish = -1;
+        step = -1;
+    }
+
+    int columnCount = 0;
+    
+    vector<float> preparedColumn;
+
+    int modelWidth = sourceModel->getWidth();
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    cerr << "modelWidth " << modelWidth << ", divisor " << divisor << endl;
+#endif
+    
+    for (int x = start; x != finish; x += step) {
+
+        // x is the on-canvas pixel coord; sx (later) will be the
+        // source column index
+        
+        ++columnCount;
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+        cerr << "x = " << x << ", binforx[x] = " << binforx[x] << endl;
+#endif
+        
+        if (binforx[x] < 0) continue;
+
+        int sx0 = binforx[x] / divisor;
+        int sx1 = sx0;
+        if (x+1 < w) sx1 = binforx[x+1] / divisor;
+        if (sx0 < 0) sx0 = sx1 - 1;
+        if (sx0 < 0) continue;
+        if (sx1 <= sx0) sx1 = sx0 + 1;
+
+        vector<float> pixelPeakColumn;
+        MagnitudeRange magRange;
+        
+        for (int sx = sx0; sx < sx1; ++sx) {
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+            cerr << "sx = " << sx << endl;
+#endif
+
+            if (sx < 0 || sx >= modelWidth) {
+                continue;
+            }
+
+            if (sx != psx) {
+                
+                // order:
+                // get column -> scale -> normalise -> record extents ->
+                // peak pick -> distribute/interpolate -> apply display gain
+
+                // this does the first three:
+                ColumnOp::Column column = getColumn(sx, minbin, nbins);
+
+                magRange.sample(column);
+                
+                if (m_params.binDisplay == BinDisplay::PeakBins) {
+                    column = ColumnOp::peakPick(column);
+                }
+
+                preparedColumn =
+                    ColumnOp::distribute(column,
+                                         h,
+                                         binfory,
+                                         minbin,
+                                         m_params.interpolate);
+
+                // Display gain belongs to the colour scale and is
+                // applied by the colour scale object when mapping it
+                
+                psx = sx;
+            }
+
+            if (sx == sx0) {
+                pixelPeakColumn = preparedColumn;
+            } else {
+                for (int i = 0; in_range_for(pixelPeakColumn, i); ++i) {
+                    pixelPeakColumn[i] = std::max(pixelPeakColumn[i],
+                                                  preparedColumn[i]);
+                }
+            }
+        }
+
+        if (!pixelPeakColumn.empty()) {
+
+            for (int y = 0; y < h; ++y) {
+                int py;
+                if (m_params.invertVertical) {
+                    py = y;
+                } else {
+                    py = h - y - 1;
+                }
+                m_drawBuffer.setPixel
+                    (x,
+                     py,
+                     m_params.colourScale.getPixel(pixelPeakColumn[y]));
+            }
+            
+            m_magRanges.push_back(magRange);
+        }
+
+        double fractionComplete = double(columnCount) / double(w);
+        if (timer.outOfTime(fractionComplete)) {
+            cerr << "out of time" << endl;
+            return columnCount;
+        }
+    }
+
+    return columnCount;
+}
+
+int
+Colour3DPlotRenderer::renderDrawBufferPeakFrequencies(const LayerGeometryProvider *v,
+                                                      int w, int h,
+                                                      const vector<int> &binforx,
+                                                      const vector<double> &binfory,
+                                                      bool rightToLeft,
+                                                      bool timeConstrained)
+{
+    // Callers must have checked that the appropriate subset of
+    // Sources data members are set for the supplied flags (e.g. that
+    // fft model exists)
+    
+    RenderTimer timer(timeConstrained ?
+                      RenderTimer::FastRender :
+                      RenderTimer::NoTimeout);
+
+    const FFTModel *fft = m_sources.fft;
+
+    int sh = fft->getHeight();
+    
+    int minbin = int(binfory[0] + 0.0001);
+    if (minbin >= sh) minbin = sh - 1;
+    if (minbin < 0) minbin = 0;
+
+    int nbins  = int(binfory[h-1]) - minbin + 1;
+    if (minbin + nbins > sh) nbins = sh - minbin;
+
+    FFTModel::PeakSet peakfreqs;
+
+    int psx = -1;
+    
+    int start = 0;
+    int finish = w;
+    int step = 1;
+
+    if (rightToLeft) {
+        start = w-1;
+        finish = -1;
+        step = -1;
+    }
+    
+    int columnCount = 0;
+    
+    vector<float> preparedColumn;
+
+    int modelWidth = fft->getWidth();
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    cerr << "modelWidth " << modelWidth << endl;
+#endif
+    
+    double minFreq =
+        (double(minbin) * fft->getSampleRate()) / fft->getFFTSize();
+    double maxFreq =
+        (double(minbin + nbins - 1) * fft->getSampleRate()) / fft->getFFTSize();
+
+    bool logarithmic = (m_params.binScale == BinScale::Log);
+    
+    for (int x = start; x != finish; x += step) {
+        
+        // x is the on-canvas pixel coord; sx (later) will be the
+        // source column index
+        
+        ++columnCount;
+        
+        if (binforx[x] < 0) continue;
+
+        int sx0 = binforx[x];
+        int sx1 = sx0;
+        if (x+1 < w) sx1 = binforx[x+1];
+        if (sx0 < 0) sx0 = sx1 - 1;
+        if (sx0 < 0) continue;
+        if (sx1 <= sx0) sx1 = sx0 + 1;
+
+        vector<float> pixelPeakColumn;
+        MagnitudeRange magRange;
+        
+        for (int sx = sx0; sx < sx1; ++sx) {
+
+            if (sx < 0 || sx >= modelWidth) {
+                continue;
+            }
+
+            if (sx != psx) {
+                preparedColumn = getColumn(sx, minbin, nbins);
+                magRange.sample(preparedColumn);
+                psx = sx;
+            }
+
+            if (sx == sx0) {
+                pixelPeakColumn = preparedColumn;
+                peakfreqs = fft->getPeakFrequencies(FFTModel::AllPeaks, sx,
+                                                    minbin, minbin + nbins - 1);
+            } else {
+                for (int i = 0; in_range_for(pixelPeakColumn, i); ++i) {
+                    pixelPeakColumn[i] = std::max(pixelPeakColumn[i],
+                                                  preparedColumn[i]);
+                }
+            }
+        }
+
+        if (!pixelPeakColumn.empty()) {
+            
+            for (FFTModel::PeakSet::const_iterator pi = peakfreqs.begin();
+                 pi != peakfreqs.end(); ++pi) {
+
+                int bin = pi->first;
+                double freq = pi->second;
+
+                if (bin < minbin) continue;
+                if (bin >= minbin + nbins) break;
+            
+                double value = pixelPeakColumn[bin - minbin];
+            
+                double y = v->getYForFrequency
+                    (freq, minFreq, maxFreq, logarithmic);
+            
+                int iy = int(y + 0.5);
+                if (iy < 0 || iy >= h) continue;
+
+                m_drawBuffer.setPixel
+                    (x,
+                     iy,
+                     m_params.colourScale.getPixel(value));
+            }
+
+            m_magRanges.push_back(magRange);
+        }
+
+        double fractionComplete = double(columnCount) / double(w);
+        if (timer.outOfTime(fractionComplete)) {
+            return columnCount;
+        }
+    }
+
+    return columnCount;
+}
+
+void
+Colour3DPlotRenderer::recreateDrawBuffer(int w, int h)
+{
+    m_drawBuffer = QImage(w, h, QImage::Format_Indexed8);
+
+    for (int pixel = 0; pixel < 256; ++pixel) {
+        m_drawBuffer.setColor
+            ((unsigned char)pixel,
+             m_params.colourScale.getColourForPixel
+             (pixel, m_params.colourRotation).rgb());
+    }
+
+    m_drawBuffer.fill(0);
+    m_magRanges.clear();
+}
+
+void
+Colour3DPlotRenderer::clearDrawBuffer(int w, int h)
+{
+    if (m_drawBuffer.width() < w || m_drawBuffer.height() != h) {
+        recreateDrawBuffer(w, h);
+    } else {
+        m_drawBuffer.fill(0);
+        m_magRanges.clear();
+    }
+}
+
+QRect
+Colour3DPlotRenderer::findSimilarRegionExtents(QPoint p) const
+{
+    QImage image = m_cache.getImage();
+    ImageRegionFinder finder;
+    QRect rect = finder.findRegionExtents(&image, p);
+    return rect;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/Colour3DPlotRenderer.h	Fri Aug 19 15:58:57 2016 +0100
@@ -0,0 +1,309 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef COLOUR_3D_PLOT_RENDERER_H
+#define COLOUR_3D_PLOT_RENDERER_H
+
+#include "ColourScale.h"
+#include "ScrollableImageCache.h"
+#include "ScrollableMagRangeCache.h"
+
+#include "base/ColumnOp.h"
+#include "base/MagnitudeRange.h"
+
+#include <QRect>
+#include <QPainter>
+#include <QImage>
+
+class LayerGeometryProvider;
+class VerticalBinLayer;
+class DenseThreeDimensionalModel;
+class Dense3DModelPeakCache;
+class FFTModel;
+
+enum class BinDisplay {
+    AllBins,
+    PeakBins,
+    PeakFrequencies
+};
+
+enum class BinScale {
+    Linear,
+    Log
+};
+
+class Colour3DPlotRenderer
+{
+public:
+    struct Sources {
+        Sources() : verticalBinLayer(0), source(0), peaks(0), fft(0) { }
+        
+        // These must all outlive this class
+        const VerticalBinLayer *verticalBinLayer;  // always
+	const DenseThreeDimensionalModel *source;  // always
+	const Dense3DModelPeakCache *peaks;        // optionally
+	const FFTModel *fft;                       // optionally
+    };        
+
+    struct Parameters {
+	Parameters() :
+	    colourScale(ColourScale::Parameters()),
+	    normalization(ColumnNormalization::None),
+	    binDisplay(BinDisplay::AllBins),
+            binScale(BinScale::Linear),
+	    alwaysOpaque(false),
+            interpolate(false),
+            invertVertical(false),
+            scaleFactor(1.0),
+            colourRotation(0) { }
+
+        /** A complete ColourScale object by value, used for colour
+         *  map conversion. Note that the final display gain setting is
+         *  also encapsulated here. */
+	ColourScale colourScale;
+
+        /** Type of column normalization. */
+	ColumnNormalization normalization;
+
+        /** Selection of bins to display. */
+	BinDisplay binDisplay;
+
+        /** Scale for vertical bin spacing (linear or logarithmic). */
+	BinScale binScale;
+
+        /** Whether cells should always be opaque. If false, then
+         *  large cells (when zoomed in a long way) will be rendered
+         *  translucent in order not to obscure anything in a layer
+         *  beneath. */
+	bool alwaysOpaque;
+
+        /** Whether to apply smoothing when rendering cells at more
+         *  than one pixel per cell.  !!! todo: decide about separating
+         *  out x-interpolate and y-interpolate as the spectrogram
+         *  actually does (or used to)
+         */
+	bool interpolate;
+
+        /** Whether to render the whole caboodle upside-down. */
+	bool invertVertical;
+
+        /** Initial scale factor (e.g. for FFT scaling). This factor
+         *  is applied to all values read from the underlying model
+         *  *before* magnitude ranges are calculated, in contrast to
+         *  the display gain found in the ColourScale parameter. */
+        double scaleFactor;
+
+        /** Colourmap rotation, in the range 0-255. */
+        int colourRotation;
+    };
+    
+    Colour3DPlotRenderer(Sources sources, Parameters parameters) :
+        m_sources(sources),
+	m_params(parameters)
+    { }
+
+    struct RenderResult {
+        /**
+         * The rect that was actually rendered. May be equal to the
+         * rect that was requested to render, or may be smaller if
+         * time ran out and the complete flag was not set.
+         */
+        QRect rendered;
+
+        /**
+         * The magnitude range of the data in the rendered area.
+         */
+        MagnitudeRange range;
+    };
+
+    /**
+     * Render the requested area using the given painter, obtaining
+     * geometry (e.g. start frame) from the given
+     * LayerGeometryProvider.
+     *
+     * The whole of the supplied rect will be rendered and the
+     * returned QRect will be equal to the supplied QRect. (See
+     * renderTimeConstrained for an alternative that may render only
+     * part of the rect in cases where obtaining source data is slow
+     * and retaining responsiveness is important.)
+     *
+     * Note that Colour3DPlotRenderer retains internal cache state
+     * related to the size and position of the supplied
+     * LayerGeometryProvider. Although it is valid to call render()
+     * successively on the same Colour3DPlotRenderer with different
+     * LayerGeometryProviders, it will be much faster to use a
+     * dedicated Colour3DPlotRenderer for each LayerGeometryProvider.
+     *
+     * If the model to render from is not ready, this will throw a
+     * std::logic_error exception. The model must be ready and the
+     * layer requesting the render must not be dormant in its view, so
+     * that the LayerGeometryProvider returns valid results; it is the
+     * caller's responsibility to ensure these.
+     */
+    RenderResult render(const LayerGeometryProvider *v,
+                        QPainter &paint, QRect rect);
+    
+    /**
+     * Render the requested area using the given painter, obtaining
+     * geometry (e.g. start frame) from the stored
+     * LayerGeometryProvider.
+     *
+     * As much of the rect will be rendered as can be managed given
+     * internal time constraints (using a RenderTimer object
+     * internally). The returned QRect (the rendered field in the
+     * RenderResult struct) will contain the area that was
+     * rendered. Note that we always render the full requested height,
+     * it's only width that is time-constrained.
+     *
+     * Note that Colour3DPlotRenderer retains internal cache state
+     * related to the size and position of the supplied
+     * LayerGeometryProvider. Although it is valid to call render()
+     * successively on the same Colour3DPlotRenderer with different
+     * LayerGeometryProviders, it will be much faster to use a
+     * dedicated Colour3DPlotRenderer for each LayerGeometryProvider.
+     *
+     * If the model to render from is not ready, this will throw a
+     * std::logic_error exception. The model must be ready and the
+     * layer requesting the render must not be dormant in its view, so
+     * that the LayerGeometryProvider returns valid results; it is the
+     * caller's responsibility to ensure these.
+     */
+    RenderResult renderTimeConstrained(const LayerGeometryProvider *v,
+                                       QPainter &paint, QRect rect);
+
+    /**
+     * Return the area of the largest rectangle within the entire area
+     * of the cache that is unavailable in the cache. This is only
+     * valid in relation to a preceding render() call which is
+     * presumed to have set the area, start frame, and zoom level for
+     * the cache. It could be used to establish a suitable region for
+     * a subsequent paint request (because if an area is not in the
+     * cache, it cannot have been rendered since the cache was
+     * cleared).
+     *
+     * Returns an empty QRect if the cache is entirely valid.
+     */
+    QRect getLargestUncachedRect(const LayerGeometryProvider *v);
+
+    /**
+     * Return true if the provider's geometry differs from the cache,
+     * or if we are not using a cache. i.e. if the cache will be
+     * regenerated for the next render, or the next render performed
+     * from scratch.
+     */
+    bool geometryChanged(const LayerGeometryProvider *v);
+    
+    /**
+     * Return true if the rendering will be opaque. This may be used
+     * by the calling layer to determine whether it can scroll
+     * directly without regard to any other layers beneath.
+     */
+    bool willRenderOpaque(const LayerGeometryProvider *v) {
+        return decideRenderType(v) != DirectTranslucent;
+    }
+    
+    /**
+     * Return the colour corresponding to the given value.
+     * \see ColourScale::getPixel
+     * \see ColourScale::getColour
+     */
+    QColor getColour(double value) const {
+        return m_params.colourScale.getColour(value, m_params.colourRotation);
+    }
+
+    /**
+     * Return the enclosing rectangle for the region of similar colour
+     * to the given point within the cache. Return an empty QRect if
+     * this is not possible. \see ImageRegionFinder
+     */
+    QRect findSimilarRegionExtents(QPoint point) const;
+    
+private:
+    Sources m_sources;
+    Parameters m_params;
+
+    // Draw buffer is the target of each partial repaint. It is always
+    // at view height (not model height) and is cleared and repainted
+    // on each fragment render. The only reason it's stored as a data
+    // member is to avoid reallocation.
+    QImage m_drawBuffer;
+
+    // A temporary store of magnitude ranges per-column, used when
+    // rendering to the draw buffer. This always has the same length
+    // as the width of the draw buffer, and the x coordinates of the
+    // two containers are equivalent.
+    std::vector<MagnitudeRange> m_magRanges;
+    
+    // The image cache is our persistent record of the visible
+    // area. It is always the same size as the view (i.e. the paint
+    // size reported by the LayerGeometryProvider) and is scrolled and
+    // partially repainted internally as appropriate. A render request
+    // is carried out by repainting to cache (via the draw buffer) any
+    // area that is being requested but is not valid in the cache, and
+    // then repainting from cache to the requested painter.
+    ScrollableImageCache m_cache;
+
+    // The mag range cache is our record of the column magnitude
+    // ranges for each of the columns in the cache. It always has the
+    // same start frame and width as the image cache, and the column
+    // indices match up across both. Our cache update mechanism
+    // guarantees that every valid column in the image cache has a
+    // valid range in the magnitude cache, but not necessarily vice
+    // versa (as the image cache is limited to contiguous ranges).
+    ScrollableMagRangeCache m_magCache;
+    
+    RenderResult render(const LayerGeometryProvider *v,
+                        QPainter &paint, QRect rect, bool timeConstrained);
+
+    MagnitudeRange renderDirectTranslucent(const LayerGeometryProvider *v,
+                                           QPainter &paint, QRect rect);
+    
+    void renderToCachePixelResolution(const LayerGeometryProvider *v, int x0,
+                                      int repaintWidth, bool rightToLeft,
+                                      bool timeConstrained);
+
+    void renderToCacheBinResolution(const LayerGeometryProvider *v, int x0,
+                                    int repaintWidth);
+
+    int renderDrawBuffer(int w, int h,
+                         const std::vector<int> &binforx,
+                         const std::vector<double> &binfory,
+                         bool usePeaksCache,
+                         bool rightToLeft,
+                         bool timeConstrained);
+
+    int renderDrawBufferPeakFrequencies(const LayerGeometryProvider *v,
+                                        int w, int h,
+                                        const std::vector<int> &binforx,
+                                        const std::vector<double> &binfory,
+                                        bool rightToLeft,
+                                        bool timeConstrained);
+    
+    void recreateDrawBuffer(int w, int h);
+    void clearDrawBuffer(int w, int h);
+
+    enum RenderType {
+        DrawBufferPixelResolution,
+        DrawBufferBinResolution,
+        DirectTranslucent
+    };
+
+    RenderType decideRenderType(const LayerGeometryProvider *) const;
+
+    ColumnOp::Column getColumn(int sx, int minbin, int nbins) const;
+};
+
+#endif
+
--- a/layer/ColourMapper.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/ColourMapper.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -21,8 +21,45 @@
 
 #include "base/Debug.h"
 
+#include <vector>
+
+using namespace std;
+
+static vector<QColor> convertStrings(const vector<QString> &strs)
+{
+    vector<QColor> converted;
+    for (const auto &s: strs) converted.push_back(QColor(s));
+    reverse(converted.begin(), converted.end());
+    return converted;
+}
+
+static vector<QColor> ice = convertStrings({
+        // Based on ColorBrewer ylGnBu
+        "#ffffff", "#ffff00", "#f7fcf0", "#e0f3db", "#ccebc5", "#a8ddb5",
+        "#7bccc4", "#4eb3d3", "#2b8cbe", "#0868ac", "#084081", "#042040"
+        });
+
+static vector<QColor> cherry = convertStrings({
+        "#f7f7f7", "#fddbc7", "#f4a582", "#d6604d", "#b2182b", "#dd3497",
+        "#ae017e", "#7a0177", "#49006a"
+        });
+    
+static void
+mapDiscrete(double norm, vector<QColor> &colours, double &r, double &g, double &b)
+{
+    int n = int(colours.size());
+    double m = norm * (n-1);
+    if (m >= n-1) { colours[n-1].getRgbF(&r, &g, &b, 0); return; }
+    if (m <= 0) { colours[0].getRgbF(&r, &g, &b, 0); return; }
+    int base(int(floor(m)));
+    double prop0 = (base + 1.0) - m, prop1 = m - base;
+    QColor c0(colours[base]), c1(colours[base+1]);
+    r = c0.redF() * prop0 + c1.redF() * prop1;
+    g = c0.greenF() * prop0 + c1.greenF() * prop1;
+    b = c0.blueF() * prop0 + c1.blueF() * prop1;
+}
+
 ColourMapper::ColourMapper(int map, double min, double max) :
-    QObject(),
     m_map(map),
     m_min(min),
     m_max(max)
@@ -47,25 +84,25 @@
 QString
 ColourMapper::getColourMapName(int n)
 {
-    if (n >= getColourMapCount()) return tr("<unknown>");
+    if (n >= getColourMapCount()) return QObject::tr("<unknown>");
     StandardMap map = (StandardMap)n;
 
     switch (map) {
-    case DefaultColours:   return tr("Default");
-    case WhiteOnBlack:     return tr("White on Black");
-    case BlackOnWhite:     return tr("Black on White");
-    case RedOnBlue:        return tr("Red on Blue");
-    case YellowOnBlack:    return tr("Yellow on Black");
-    case BlueOnBlack:      return tr("Blue on Black");
-    case Sunset:           return tr("Sunset");
-    case FruitSalad:       return tr("Fruit Salad");
-    case Banded:           return tr("Banded");
-    case Highlight:        return tr("Highlight");
-    case Printer:          return tr("Printer");
-    case HighGain:         return tr("High Gain");
+    case Green:            return QObject::tr("Green");
+    case WhiteOnBlack:     return QObject::tr("White on Black");
+    case BlackOnWhite:     return QObject::tr("Black on White");
+    case Cherry:           return QObject::tr("Cherry");
+    case Wasp:             return QObject::tr("Wasp");
+    case Ice:              return QObject::tr("Ice");
+    case Sunset:           return QObject::tr("Sunset");
+    case FruitSalad:       return QObject::tr("Fruit Salad");
+    case Banded:           return QObject::tr("Banded");
+    case Highlight:        return QObject::tr("Highlight");
+    case Printer:          return QObject::tr("Printer");
+    case HighGain:         return QObject::tr("High Gain");
     }
 
-    return tr("<unknown>");
+    return QObject::tr("<unknown>");
 }
 
 QColor
@@ -85,7 +122,7 @@
 
     switch (map) {
 
-    case DefaultColours:
+    case Green:
         h = blue - norm * 2.0 * pieslice;
         s = 0.5f + norm/2.0;
         v = norm;
@@ -101,30 +138,17 @@
         hsv = false;
         break;
 
-    case RedOnBlue:
-        h = blue - pieslice/4.0 + norm * (pieslice + pieslice/4.0);
-        s = 1.0;
-        v = norm;
+    case Cherry:
+        hsv = false;
+        mapDiscrete(norm, cherry, r, g, b);
         break;
 
-    case YellowOnBlack:
+    case Wasp:
         h = 0.15;
         s = 1.0;
         v = norm;
         break;
 
-    case BlueOnBlack:
-        h = blue;
-        s = 1.0;
-        v = norm * 2.0;
-        if (v > 1.0) {
-            v = 1.0;
-            s = 1.0 - (sqrt(norm) - 0.707) * 3.413;
-            if (s < 0.0) s = 0.0;
-            if (s > 1.0) s = 1.0;
-        }
-        break;
-
     case Sunset:
         r = (norm - 0.24) * 2.38;
         if (r > 1.0) r = 1.0;
@@ -207,6 +231,10 @@
         hsv = false;
 */
         break;
+
+    case Ice:
+        hsv = false;
+        mapDiscrete(norm, ice, r, g, b);
     }
 
     if (hsv) {
@@ -224,7 +252,7 @@
 
     switch (map) {
 
-    case DefaultColours:
+    case Green:
         return QColor(255, 150, 50);
 
     case WhiteOnBlack:
@@ -233,13 +261,13 @@
     case BlackOnWhite:
         return Qt::darkGreen;
 
-    case RedOnBlue:
+    case Cherry:
         return Qt::green;
 
-    case YellowOnBlack:
+    case Wasp:
         return QColor::fromHsv(240, 255, 255);
 
-    case BlueOnBlack:
+    case Ice:
         return Qt::red;
 
     case Sunset:
@@ -277,12 +305,12 @@
     case HighGain:
         return true;
 
-    case DefaultColours:
+    case Green:
     case Sunset:
     case WhiteOnBlack:
-    case RedOnBlue:
-    case YellowOnBlack:
-    case BlueOnBlack:
+    case Cherry:
+    case Wasp:
+    case Ice:
     case FruitSalad:
     case Banded:
     case Highlight:
--- a/layer/ColourMapper.h	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/ColourMapper.h	Fri Aug 19 15:58:57 2016 +0100
@@ -23,23 +23,23 @@
 /**
  * A class for mapping intensity values onto various colour maps.
  */
-
-class ColourMapper : public QObject
+class ColourMapper
 {
-    Q_OBJECT
-
 public:
     ColourMapper(int map, double minValue, double maxValue);
-    virtual ~ColourMapper();
+    ~ColourMapper();
+
+    ColourMapper(const ColourMapper &) = default;
+    ColourMapper &operator=(const ColourMapper &) = default;
 
     enum StandardMap {
-        DefaultColours,
+        Green,
         Sunset,
         WhiteOnBlack,
         BlackOnWhite,
-        RedOnBlue,
-        YellowOnBlack,
-        BlueOnBlack,
+        Cherry,
+        Wasp,
+        Ice,
         FruitSalad,
         Banded,
         Highlight,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ColourScale.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -0,0 +1,160 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "ColourScale.h"
+
+#include "base/AudioLevel.h"
+#include "base/LogRange.h"
+
+#include <cmath>
+#include <iostream>
+
+using namespace std;
+
+int ColourScale::m_maxPixel = 255;
+
+ColourScale::ColourScale(Parameters parameters) :
+    m_params(parameters),
+    m_mapper(m_params.colourMap, 1.f, double(m_maxPixel))
+{
+    if (m_params.minValue >= m_params.maxValue) {
+        cerr << "ERROR: ColourScale::ColourScale: minValue = "
+             << m_params.minValue << ", maxValue = " << m_params.maxValue << endl;
+	throw std::logic_error("maxValue must be greater than minValue");
+    }
+
+    m_mappedMin = m_params.minValue;
+    m_mappedMax = m_params.maxValue;
+
+    if (m_mappedMin < m_params.threshold) {
+        m_mappedMin = m_params.threshold;
+    }
+    
+    if (m_params.scaleType == ColourScaleType::Log) {
+
+	LogRange::mapRange(m_mappedMin, m_mappedMax);
+	
+    } else if (m_params.scaleType == ColourScaleType::PlusMinusOne) {
+	
+	m_mappedMin = -1.0;
+	m_mappedMax =  1.0;
+
+    } else if (m_params.scaleType == ColourScaleType::Absolute) {
+
+	m_mappedMin = fabs(m_mappedMin);
+	m_mappedMax = fabs(m_mappedMax);
+	if (m_mappedMin >= m_mappedMax) {
+	    std::swap(m_mappedMin, m_mappedMax);
+	}
+    }
+
+    if (m_mappedMin >= m_mappedMax) {
+        cerr << "ERROR: ColourScale::ColourScale: minValue = " << m_params.minValue
+             << ", maxValue = " << m_params.maxValue
+             << ", threshold = " << m_params.threshold
+             << ", scale = " << int(m_params.scaleType)
+             << " resulting in mapped minValue = " << m_mappedMin
+             << ", mapped maxValue = " << m_mappedMax << endl;
+	throw std::logic_error("maxValue must be greater than minValue [after mapping]");
+    }
+}
+
+ColourScale::~ColourScale()
+{
+}
+
+ColourScaleType
+ColourScale::getScale() const
+{
+    return m_params.scaleType;
+}
+
+int
+ColourScale::getPixel(double value) const
+{
+    double maxPixF = m_maxPixel;
+
+    if (m_params.scaleType == ColourScaleType::Phase) {
+	double half = (maxPixF - 1.f) / 2.f;
+        int pixel = 1 + int((value * half) / M_PI + half);
+//        cerr << "phase = " << value << " pixel = " << pixel << endl;
+        return pixel;
+    }
+    
+    value *= m_params.gain;
+    
+    if (value < m_params.threshold) return 0;
+
+    double mapped = value;
+
+    if (m_params.scaleType == ColourScaleType::Log) {
+	mapped = LogRange::map(value);
+    } else if (m_params.scaleType == ColourScaleType::PlusMinusOne) {
+	if (mapped < -1.f) mapped = -1.f;
+	if (mapped > 1.f) mapped = 1.f;
+    } else if (m_params.scaleType == ColourScaleType::Absolute) {
+	if (mapped < 0.f) mapped = -mapped;
+    }
+
+    mapped *= m_params.multiple;
+    
+    if (mapped < m_mappedMin) {
+	mapped = m_mappedMin;
+    }
+    if (mapped > m_mappedMax) {
+	mapped = m_mappedMax;
+    }
+
+    double proportion = (mapped - m_mappedMin) / (m_mappedMax - m_mappedMin);
+
+    int pixel = 0;
+
+    if (m_params.scaleType == ColourScaleType::Meter) {
+	pixel = AudioLevel::multiplier_to_preview(proportion, m_maxPixel-1) + 1;
+    } else {
+	pixel = int(proportion * maxPixF) + 1;
+    }
+
+    if (pixel < 0) {
+	pixel = 0;
+    }
+    if (pixel > m_maxPixel) {
+	pixel = m_maxPixel;
+    }
+    return pixel;
+}
+
+QColor
+ColourScale::getColourForPixel(int pixel, int rotation) const
+{
+    if (pixel < 0) {
+	pixel = 0;
+    }
+    if (pixel > m_maxPixel) {
+	pixel = m_maxPixel;
+    }
+    if (pixel == 0) {
+	if (m_mapper.hasLightBackground()) {
+	    return Qt::white;
+	} else {
+	    return Qt::black;
+	}
+    } else {
+	int target = int(pixel) + rotation;
+	while (target < 1) target += m_maxPixel;
+	while (target > m_maxPixel) target -= m_maxPixel;
+	return m_mapper.map(double(target));
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ColourScale.h	Fri Aug 19 15:58:57 2016 +0100
@@ -0,0 +1,120 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef COLOUR_SCALE_H
+#define COLOUR_SCALE_H
+
+#include "ColourMapper.h"
+
+enum class ColourScaleType {
+    Linear,
+    Meter,
+    Log,
+    Phase,
+    PlusMinusOne,
+    Absolute
+};
+
+/**
+ * Map values within a range onto a set of colours, with a given
+ * distribution (linear, log etc) and optional colourmap rotation.
+ */
+class ColourScale
+{
+public:
+    struct Parameters {
+	Parameters() : colourMap(0), scaleType(ColourScaleType::Linear),
+		       minValue(0.0), maxValue(1.0),
+		       threshold(0.0), gain(1.0), multiple(1.0) { }
+
+	/** A colour map index as used by ColourMapper */
+	int colourMap;
+	
+	/** Distribution for the scale */
+	ColourScaleType scaleType;
+	
+	/** Minimum value in source range */
+	double minValue;
+	
+	/** Maximum value in source range. Must be > minValue */
+	double maxValue;
+
+	/** Threshold below which every value is mapped to background
+	    pixel 0 */
+	double threshold;
+
+	/** Gain to apply before thresholding, mapping, and clamping */
+	double gain;
+
+        /** Multiple to apply after thresholding and mapping. In most
+         *  cases the gain parameter is the one you want instead of
+         *  this, but this can be used for example with Log scale to
+         *  produce the log of some power of the original value,
+         *  e.g. multiple = 2 gives log(x^2). */
+        double multiple;
+    };
+    
+    /**
+     * Create a ColourScale with the given parameters.
+     *
+     * Note that some parameters may be ignored for some scale
+     * distribution settings. For example, min and max are ignored for
+     * PlusMinusOneScale and PhaseColourScale and threshold and gain
+     * are ignored for PhaseColourScale.
+     */
+    ColourScale(Parameters parameters);
+    ~ColourScale();
+
+    ColourScale(const ColourScale &) = default;
+    ColourScale &operator=(const ColourScale &) = default;
+
+    /**
+     * Return the general type of scale this is.
+     */
+    ColourScaleType getScale() const;
+    
+    /**
+     * Return a pixel number (in the range 0-255 inclusive)
+     * corresponding to the given value.  The pixel 0 is used only for
+     * values below the threshold supplied in the constructor. All
+     * other values are mapped onto the range 1-255.
+     */
+    int getPixel(double value) const;
+
+    /**
+     * Return the colour for the given pixel number (which must be in
+     * the range 0-255). The pixel 0 is always the background
+     * colour. Other pixels are mapped taking into account the given
+     * colourmap rotation (which is also a value in the range 0-255).
+     */
+    QColor getColourForPixel(int pixel, int rotation) const;
+    
+    /**
+     * Return the colour corresponding to the given value. This is
+     * equivalent to getColourForPixel(getPixel(value), rotation).
+     */
+    QColor getColour(double value, int rotation) const {
+	return getColourForPixel(getPixel(value), rotation);
+    }
+
+private:
+    Parameters m_params;
+    ColourMapper m_mapper;
+    double m_mappedMin;
+    double m_mappedMax;
+    static int m_maxPixel;
+};
+
+#endif
--- a/layer/ColourScaleLayer.h	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/ColourScaleLayer.h	Fri Aug 19 15:58:57 2016 +0100
@@ -21,6 +21,13 @@
 
 class LayerGeometryProvider;
 
+/**
+ * Interface for layers in which a colour scale represents (or can
+ * sometimes represent, depending on the display mode) the sample
+ * value. For example, TimeValueLayer uses colour scale when in
+ * segment mode and so provides this interface for use by the
+ * LogColourScale or LinearColourScale scale renderers.
+ */
 class ColourScaleLayer
 {
 public:
--- a/layer/FlexiNoteLayer.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/FlexiNoteLayer.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -22,15 +22,18 @@
 #include "base/Pitch.h"
 #include "base/LogRange.h"
 #include "base/RangeMapper.h"
+
 #include "ColourDatabase.h"
-#include "view/View.h"
-
+#include "LayerGeometryProvider.h"
 #include "PianoScale.h"
 #include "LinearNumericalScale.h"
 #include "LogNumericalScale.h"
+#include "PaintAssistant.h"
 
 #include "data/model/FlexiNoteModel.h"
 
+#include "view/View.h"
+
 #include "widgets/ItemEditDialog.h"
 #include "widgets/TextAbbrev.h"
 
@@ -867,34 +870,34 @@
                 // paint.setBrush(v->getForeground());
         
                 QString vlabel = QString("freq: %1%2").arg(p.value).arg(m_model->getScaleUnits());
-                // v->drawVisibleText(paint, 
+                // PaintAssistant::drawVisibleText(v, paint, 
                 //                    x - paint.fontMetrics().width(vlabel) - 2,
                 //                    y + paint.fontMetrics().height()/2
                 //                      - paint.fontMetrics().descent(), 
-                //                    vlabel, View::OutlinedText);
-                v->drawVisibleText(paint, 
+                //                    vlabel, PaintAssistant::OutlinedText);
+                PaintAssistant::drawVisibleText(v, paint, 
                                    x,
                                    y - h/2 - 2 - paint.fontMetrics().height()
                                      - paint.fontMetrics().descent(), 
-                                   vlabel, View::OutlinedText);
+                                   vlabel, PaintAssistant::OutlinedText);
 
                 QString hlabel = "dur: " + QString(RealTime::frame2RealTime
                     (p.duration, m_model->getSampleRate()).toText(true).c_str());
-                v->drawVisibleText(paint, 
+                PaintAssistant::drawVisibleText(v, paint, 
                                    x,
                                    y - h/2 - paint.fontMetrics().descent() - 2,
-                                   hlabel, View::OutlinedText);
+                                   hlabel, PaintAssistant::OutlinedText);
 
                 QString llabel = QString("%1").arg(p.label);
-                v->drawVisibleText(paint, 
+                PaintAssistant::drawVisibleText(v, paint, 
                                    x,
                                    y + h + 2 + paint.fontMetrics().descent(),
-                                   llabel, View::OutlinedText);
+                                   llabel, PaintAssistant::OutlinedText);
                 QString nlabel = QString("%1").arg(noteNumber);
-                v->drawVisibleText(paint, 
+                PaintAssistant::drawVisibleText(v, paint, 
                                    x + paint.fontMetrics().averageCharWidth() / 2,
                                    y + h/2 - paint.fontMetrics().descent(),
-                                   nlabel, View::OutlinedText);
+                                   nlabel, PaintAssistant::OutlinedText);
         }
     
         paint.drawRect(x, y - h/2, w, h);
--- a/layer/LayerFactory.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/LayerFactory.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -289,6 +289,7 @@
     if (name == "timeruler") return TimeRuler;
     if (name == "timeinstants") return TimeInstants;
     if (name == "timevalues") return TimeValues;
+    if (name == "notes") return Notes;
     if (name == "flexinotes") return FlexiNotes;
     if (name == "regions") return Regions;
     if (name == "text") return Text;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/LayerGeometryProvider.h	Fri Aug 19 15:58:57 2016 +0100
@@ -0,0 +1,184 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef LAYER_GEOMETRY_PROVIDER_H
+#define LAYER_GEOMETRY_PROVIDER_H
+
+#include "base/BaseTypes.h"
+
+#include <QMutex>
+#include <QMutexLocker>
+#include <QPainter>
+
+class ViewManager;
+class View;
+class Layer;
+
+/**
+ * Interface for classes that provide geometry information (such as
+ * size, start frame, and a large number of other properties) about
+ * the disposition of a layer. The main implementor of this interface
+ * is the View class, but other implementations may be used in
+ * different circumstances, e.g. as a proxy to handle hi-dpi
+ * coordinate mapping.
+ *
+ * Note it is expected that some implementations of this may be
+ * disposable, created on-the-fly for a single use. Code that receives
+ * a LayerGeometryProvider pointer as an argument to something should
+ * not, in general, store that pointer as it may be invalidated before
+ * the next use. Use getId() to instead obtain a persistent identifier
+ * for a LayerGeometryProvider, for example to establish whether the
+ * same one is being provided in two separate calls.
+ */
+class LayerGeometryProvider
+{
+protected:
+    static int getNextId() {
+        static QMutex idMutex;
+        static int nextId = 1;
+        static int maxId = INT_MAX;
+        QMutexLocker locker(&idMutex);
+        int id = nextId;
+        if (nextId == maxId) {
+            // we don't expect this to happen in the lifetime of a
+            // process, but it would be undefined behaviour if it did
+            // since we're using a signed int, so we should really
+            // guard for it...
+            nextId = 1;
+        } else {
+            nextId++;
+        }
+        return id;
+    }            
+    
+public:
+    LayerGeometryProvider() { }
+    
+    /**
+     * Retrieve the id of this object.
+     */
+    virtual int getId() const = 0;
+
+    /**
+     * Retrieve the first visible sample frame on the widget.
+     * This is a calculated value based on the centre-frame, widget
+     * width and zoom level.  The result may be negative.
+     */
+    virtual sv_frame_t getStartFrame() const = 0;
+
+    /**
+     * Return the centre frame of the visible widget.  This is an
+     * exact value that does not depend on the zoom block size.  Other
+     * frame values (start, end) are calculated from this based on the
+     * zoom and other factors.
+     */
+    virtual sv_frame_t getCentreFrame() const = 0;
+
+    /**
+     * Retrieve the last visible sample frame on the widget.
+     * This is a calculated value based on the centre-frame, widget
+     * width and zoom level.
+     */
+    virtual sv_frame_t getEndFrame() const = 0;
+
+    /**
+     * Return the pixel x-coordinate corresponding to a given sample
+     * frame (which may be negative).
+     */
+    virtual int getXForFrame(sv_frame_t frame) const = 0;
+
+    /**
+     * Return the closest frame to the given pixel x-coordinate.
+     */
+    virtual sv_frame_t getFrameForX(int x) const = 0;
+
+    virtual sv_frame_t getModelsStartFrame() const = 0;
+    virtual sv_frame_t getModelsEndFrame() const = 0;
+
+    /**
+     * Return the closest pixel x-coordinate corresponding to a given
+     * view x-coordinate.
+     */
+    virtual int getXForViewX(int viewx) const = 0;
+    
+    /**
+     * Return the closest view x-coordinate corresponding to a given
+     * pixel x-coordinate.
+     */
+    virtual int getViewXForX(int x) const = 0;
+    
+    /**
+     * Return the (maybe fractional) pixel y-coordinate corresponding
+     * to a given frequency, if the frequency range is as specified.
+     * This does not imply any policy about layer frequency ranges,
+     * but it might be useful for layers to match theirs up if
+     * desired.
+     *
+     * Not thread-safe in logarithmic mode.  Call only from GUI thread.
+     */
+    virtual double getYForFrequency(double frequency,
+                                    double minFreq, double maxFreq, 
+                                    bool logarithmic) const = 0;
+
+    /**
+     * Return the closest frequency to the given (maybe fractional)
+     * pixel y-coordinate, if the frequency range is as specified.
+     *
+     * Not thread-safe in logarithmic mode.  Call only from GUI thread.
+     */
+    virtual double getFrequencyForY(double y,
+                                    double minFreq, double maxFreq,
+                                    bool logarithmic) const = 0;
+
+    virtual int getTextLabelHeight(const Layer *layer, QPainter &) const = 0;
+
+    virtual bool getValueExtents(QString unit, double &min, double &max,
+                                 bool &log) const = 0;
+
+    /**
+     * Return the zoom level, i.e. the number of frames per pixel
+     */
+    virtual int getZoomLevel() const = 0;
+
+    /**
+     * To be called from a layer, to obtain the extent of the surface
+     * that the layer is currently painting to. This may be the extent
+     * of the view (if 1x display scaling is in effect) or of a larger
+     * cached pixmap (if greater display scaling is in effect).
+     */
+    virtual QRect getPaintRect() const = 0;
+
+    virtual QSize getPaintSize() const { return getPaintRect().size(); }
+    virtual int getPaintWidth() const { return getPaintRect().width(); }
+    virtual int getPaintHeight() const { return getPaintRect().height(); }
+
+    virtual bool hasLightBackground() const = 0;
+    virtual QColor getForeground() const = 0;
+    virtual QColor getBackground() const = 0;
+
+    virtual ViewManager *getViewManager() const = 0;
+
+    virtual bool shouldIlluminateLocalFeatures(const Layer *, QPoint &) const = 0;
+    virtual bool shouldShowFeatureLabels() const = 0;
+
+    virtual void drawMeasurementRect(QPainter &p, const Layer *,
+                                     QRect rect, bool focus) const = 0;
+
+    virtual void updatePaintRect(QRect r) = 0;
+    
+    virtual View *getView() = 0;
+    virtual const View *getView() const = 0;
+};
+
+#endif
--- a/layer/LinearColourScale.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/LinearColourScale.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -20,7 +20,7 @@
 
 #include <cmath>
 
-#include "view/LayerGeometryProvider.h"
+#include "LayerGeometryProvider.h"
 
 int
 LinearColourScale::getWidth(LayerGeometryProvider *,
--- a/layer/LinearNumericalScale.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/LinearNumericalScale.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -20,7 +20,7 @@
 
 #include <cmath>
 
-#include "view/View.h"
+#include "LayerGeometryProvider.h"
 
 int
 LinearNumericalScale::getWidth(LayerGeometryProvider *,
--- a/layer/LogColourScale.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/LogColourScale.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -22,7 +22,7 @@
 
 #include <cmath>
 
-#include "view/View.h"
+#include "LayerGeometryProvider.h"
 
 int
 LogColourScale::getWidth(LayerGeometryProvider *,
--- a/layer/LogNumericalScale.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/LogNumericalScale.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -22,7 +22,7 @@
 
 #include <cmath>
 
-#include "view/View.h"
+#include "LayerGeometryProvider.h"
 
 //#define DEBUG_TIME_VALUE_LAYER 1
 
--- a/layer/NoteLayer.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/NoteLayer.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -21,12 +21,13 @@
 #include "base/Pitch.h"
 #include "base/LogRange.h"
 #include "base/RangeMapper.h"
-#include "ColourDatabase.h"
 #include "view/View.h"
 
+#include "ColourDatabase.h"
 #include "PianoScale.h"
 #include "LinearNumericalScale.h"
 #include "LogNumericalScale.h"
+#include "PaintAssistant.h"
 
 #include "data/model/NoteModel.h"
 
@@ -809,18 +810,18 @@
             paint.setBrush(v->getForeground());
 
             QString vlabel = QString("%1%2").arg(p.value).arg(getScaleUnits());
-            v->drawVisibleText(paint, 
+            PaintAssistant::drawVisibleText(v, paint, 
                                x - paint.fontMetrics().width(vlabel) - 2,
                                y + paint.fontMetrics().height()/2
                                  - paint.fontMetrics().descent(), 
-                               vlabel, View::OutlinedText);
+                               vlabel, PaintAssistant::OutlinedText);
 
             QString hlabel = RealTime::frame2RealTime
                 (p.frame, m_model->getSampleRate()).toText(true).c_str();
-            v->drawVisibleText(paint, 
+            PaintAssistant::drawVisibleText(v, paint, 
                                x,
                                y - h/2 - paint.fontMetrics().descent() - 2,
-                               hlabel, View::OutlinedText);
+                               hlabel, PaintAssistant::OutlinedText);
 	}
 	
 	paint.drawRect(x, y - h/2, w, h);
--- a/layer/PaintAssistant.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/PaintAssistant.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -15,7 +15,10 @@
 
 #include "PaintAssistant.h"
 
+#include "LayerGeometryProvider.h"
+
 #include "base/AudioLevel.h"
+#include "base/Strings.h"
 
 #include <QPaintDevice>
 #include <QPainter>
@@ -79,7 +82,7 @@
             text = QString("%1").arg(meterdbs[i]);
             if (i == n) text = "0dB";
             if (i == 0) {
-                text = "-Inf";
+                text = Strings::minus_infinity;
                 val = 0.0;
             }
             break;
@@ -89,7 +92,7 @@
             text = QString("%1").arg(-(10*n) + i * 10);
             if (i == n) text = "0dB";
             if (i == 0) {
-                text = "-Inf";
+                text = Strings::minus_infinity;
                 val = 0.0;
             }
             break;
@@ -207,3 +210,55 @@
 
     return vy;
 }
+
+void
+PaintAssistant::drawVisibleText(const LayerGeometryProvider *v,
+                                QPainter &paint, int x, int y,
+                                QString text, TextStyle style)
+{
+    if (style == OutlinedText || style == OutlinedItalicText) {
+
+        paint.save();
+
+        if (style == OutlinedItalicText) {
+            QFont f(paint.font());
+            f.setItalic(true);
+            paint.setFont(f);
+        }
+
+        QColor penColour, surroundColour, boxColour;
+
+        penColour = v->getForeground();
+        surroundColour = v->getBackground();
+        boxColour = surroundColour;
+        boxColour.setAlpha(127);
+
+        paint.setPen(Qt::NoPen);
+        paint.setBrush(boxColour);
+        
+        QRect r = paint.fontMetrics().boundingRect(text);
+        r.translate(QPoint(x, y));
+//        cerr << "drawVisibleText: r = " << r.x() << "," <<r.y() << " " << r.width() << "x" << r.height() << endl;
+        paint.drawRect(r);
+        paint.setBrush(Qt::NoBrush);
+
+	paint.setPen(surroundColour);
+
+	for (int dx = -1; dx <= 1; ++dx) {
+	    for (int dy = -1; dy <= 1; ++dy) {
+		if (!(dx || dy)) continue;
+		paint.drawText(x + dx, y + dy, text);
+	    }
+	}
+
+	paint.setPen(penColour);
+
+	paint.drawText(x, y, text);
+
+        paint.restore();
+
+    } else {
+
+        std::cerr << "ERROR: PaintAssistant::drawVisibleText: Boxed style not yet implemented!" << std::endl;
+    }
+}
--- a/layer/PaintAssistant.h	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/PaintAssistant.h	Fri Aug 19 15:58:57 2016 +0100
@@ -13,13 +13,15 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _PAINT_ASSISTANT_H_
-#define _PAINT_ASSISTANT_H_
+#ifndef PAINT_ASSISTANT_H
+#define PAINT_ASSISTANT_H
 
 #include <QRect>
 #include <vector>
 
 class QPainter;
+class Layer;
+class LayerGeometryProvider;
 
 class PaintAssistant
 {
@@ -34,6 +36,16 @@
     static int getYForValue(Scale scale, double value,
                             double minVal, double maxVal,
                             int minY, int height);
+
+    enum TextStyle {
+	BoxedText,
+	OutlinedText,
+        OutlinedItalicText
+    };
+
+    static void drawVisibleText(const LayerGeometryProvider *,
+                                QPainter &p, int x, int y,
+                                QString text, TextStyle style);
 };
 
 #endif
--- a/layer/PianoScale.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/PianoScale.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -21,7 +21,7 @@
 
 #include "base/Pitch.h"
 
-#include "view/LayerGeometryProvider.h"
+#include "LayerGeometryProvider.h"
 
 void
 PianoScale::paintPianoVertical(LayerGeometryProvider *v,
--- a/layer/RegionLayer.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/RegionLayer.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -19,13 +19,14 @@
 #include "base/RealTime.h"
 #include "base/Profiler.h"
 #include "base/LogRange.h"
+
 #include "ColourDatabase.h"
-
 #include "ColourMapper.h"
 #include "LinearNumericalScale.h"
 #include "LogNumericalScale.h"
 #include "LinearColourScale.h"
 #include "LogColourScale.h"
+#include "PaintAssistant.h"
 
 #include "view/View.h"
 
@@ -979,18 +980,18 @@
                 paint.setBrush(v->getForeground());
 
                 QString vlabel = QString("%1%2").arg(p.value).arg(getScaleUnits());
-                v->drawVisibleText(paint, 
+                PaintAssistant::drawVisibleText(v, paint, 
                                    x - paint.fontMetrics().width(vlabel) - 2,
                                    y + paint.fontMetrics().height()/2
                                    - paint.fontMetrics().descent(), 
-                                   vlabel, View::OutlinedText);
+                                   vlabel, PaintAssistant::OutlinedText);
                 
                 QString hlabel = RealTime::frame2RealTime
                     (p.frame, m_model->getSampleRate()).toText(true).c_str();
-                v->drawVisibleText(paint, 
+                PaintAssistant::drawVisibleText(v, paint, 
                                    x,
                                    y - h/2 - paint.fontMetrics().descent() - 2,
-                                   hlabel, View::OutlinedText);
+                                   hlabel, PaintAssistant::OutlinedText);
             }
             
             paint.drawLine(x, y-1, x + w, y-1);
@@ -1048,7 +1049,7 @@
                 nextLabelMinX = labelX + paint.fontMetrics().width(label);
             }
 
-            v->drawVisibleText(paint, labelX, labelY, label, View::OutlinedText);
+            PaintAssistant::drawVisibleText(v, paint, labelX, labelY, label, PaintAssistant::OutlinedText);
         }
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/RenderTimer.h	Fri Aug 19 15:58:57 2016 +0100
@@ -0,0 +1,102 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef RENDER_TIMER_H
+#define RENDER_TIMER_H
+
+#include <chrono>
+
+class RenderTimer
+{
+public:
+    enum Type {
+	/// A normal rendering operation with normal responsiveness demands
+	FastRender,
+
+	/// An operation that the user might accept being slower
+	SlowRender,
+
+	/// An operation that should always complete, i.e. as if there
+	/// were no RenderTimer in use, but without having to change
+	/// client code structurally
+	NoTimeout
+    };
+    
+    /**
+     * Create a new RenderTimer and start timing. Make one of these
+     * before rendering, and then call outOfTime() regularly during
+     * rendering. If outOfTime() returns true, abandon rendering!  and
+     * schedule the rest for after some user responsiveness has
+     * happened.
+     */
+    RenderTimer(Type t) :
+	m_start(std::chrono::steady_clock::now()),
+	m_haveLimits(true),
+	m_minFraction(0.1),
+	m_softLimit(0.1),
+	m_hardLimit(0.2),
+	m_softLimitOverridden(false) {
+
+	if (t == NoTimeout) {
+	    m_haveLimits = false;
+	} else if (t == SlowRender) {
+	    m_softLimit = 0.2;
+	    m_hardLimit = 0.4;
+	}
+    }
+
+
+    /**
+     * Return true if we have run out of time and should suspend
+     * rendering and handle user events instead. Call this regularly
+     * during rendering work: fractionComplete should be an estimate
+     * of how much of the work has been done as of this call, as a
+     * number between 0.0 (none of it) and 1.0 (all of it).
+     */
+    bool outOfTime(double fractionComplete) {
+
+	if (!m_haveLimits || fractionComplete < m_minFraction) {
+	    return false;
+	}
+	
+	auto t = std::chrono::steady_clock::now();
+	double elapsed = std::chrono::duration<double>(t - m_start).count();
+	
+	if (elapsed > m_hardLimit) {
+	    return true;
+	} else if (!m_softLimitOverridden && elapsed > m_softLimit) {
+	    if (fractionComplete > 0.6) {
+		// If we're significantly more than half way by the
+		// time we reach the soft limit, ignore it (though
+		// always respect the hard limit, above). Otherwise
+		// respect the soft limit and report out of time now.
+		m_softLimitOverridden = true;
+	    } else {
+		return true;
+	    }
+	}
+
+	return false;
+    }
+
+private:
+    std::chrono::time_point<std::chrono::steady_clock> m_start;
+    bool m_haveLimits;
+    double m_minFraction;
+    double m_softLimit;
+    double m_hardLimit;
+    bool m_softLimitOverridden;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ScrollableImageCache.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -0,0 +1,210 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "ScrollableImageCache.h"
+
+#include <iostream>
+using namespace std;
+
+//#define DEBUG_SCROLLABLE_IMAGE_CACHE 1
+
+void
+ScrollableImageCache::scrollTo(const LayerGeometryProvider *v,
+                               sv_frame_t newStartFrame)
+{
+    int dx = (v->getXForFrame(m_startFrame) -
+	      v->getXForFrame(newStartFrame));
+    
+#ifdef DEBUG_SCROLLABLE_IMAGE_CACHE
+    cerr << "ScrollableImageCache::scrollTo: start frame " << m_startFrame
+	 << " -> " << newStartFrame << ", dx = " << dx << endl;
+#endif
+
+    if (m_startFrame == newStartFrame) {
+	// haven't moved
+        return;
+    }
+	
+    m_startFrame = newStartFrame;
+	
+    if (!isValid()) {
+	return;
+    }
+
+    int w = m_image.width();
+
+    if (dx == 0) {
+	// haven't moved visibly (even though start frame may have changed)
+	return;
+    }
+
+    if (dx <= -w || dx >= w) {
+	// scrolled entirely off
+	invalidate();
+	return;
+    }
+	
+    // dx is in range, cache is scrollable
+
+    int dxp = dx;
+    if (dxp < 0) dxp = -dxp;
+
+    int copylen = (w - dxp) * int(sizeof(QRgb));
+    for (int y = 0; y < m_image.height(); ++y) {
+	QRgb *line = (QRgb *)m_image.scanLine(y);
+	if (dx < 0) {
+	    memmove(line, line + dxp, copylen);
+	} else {
+	    memmove(line + dxp, line, copylen);
+	}
+    }
+	
+    // update valid area
+        
+    int px = m_validLeft;
+    int pw = m_validWidth;
+	
+    px += dx;
+	
+    if (dx < 0) {
+	// we scrolled left
+	if (px < 0) {
+	    pw += px;
+	    px = 0;
+	    if (pw < 0) {
+		pw = 0;
+	    }
+	}
+    } else {
+	// we scrolled right
+	if (px + pw > w) {
+	    pw = w - px;
+	    if (pw < 0) {
+		pw = 0;
+	    }
+	}
+    }
+
+    m_validLeft = px;
+    m_validWidth = pw;
+}
+
+void
+ScrollableImageCache::adjustToTouchValidArea(int &left, int &width,
+					     bool &isLeftOfValidArea) const
+{
+#ifdef DEBUG_SCROLLABLE_IMAGE_CACHE
+    cerr << "ScrollableImageCache::adjustToTouchValidArea: left " << left
+         << ", width " << width << endl;
+    cerr << "ScrollableImageCache: my left " << m_validLeft
+         << ", width " << m_validWidth << " so right " << (m_validLeft + m_validWidth) << endl;
+#endif
+    if (left < m_validLeft) {
+	isLeftOfValidArea = true;
+	if (left + width <= m_validLeft + m_validWidth) {
+	    width = m_validLeft - left;
+	}
+#ifdef DEBUG_SCROLLABLE_IMAGE_CACHE
+        cerr << "ScrollableImageCache: we're left of valid area, adjusted width to " << width << endl;
+#endif
+    } else {
+	isLeftOfValidArea = false;
+	width = left + width - (m_validLeft + m_validWidth);
+	left = m_validLeft + m_validWidth;
+	if (width < 0) width = 0;
+#ifdef DEBUG_SCROLLABLE_IMAGE_CACHE
+        cerr << "ScrollableImageCache: we're right of valid area, adjusted left to " << left << ", width to " << width << endl;
+#endif
+    }
+}
+    
+void
+ScrollableImageCache::drawImage(int left,
+				int width,
+				QImage image,
+				int imageLeft,
+				int imageWidth)
+{
+    if (image.height() != m_image.height()) {
+	cerr << "ScrollableImageCache::drawImage: ERROR: Supplied image height "
+	     << image.height() << " does not match cache height "
+	     << m_image.height() << endl;
+	throw std::logic_error("Image height must match cache height in ScrollableImageCache::drawImage");
+    }
+    if (left < 0 || width < 0 || left + width > m_image.width()) {
+	cerr << "ScrollableImageCache::drawImage: ERROR: Target area (left = "
+	     << left << ", width = " << width << ", so right = " << left + width
+             << ") out of bounds for cache of width " << m_image.width() << endl;
+	throw std::logic_error("Target area out of bounds in ScrollableImageCache::drawImage");
+    }
+    if (imageLeft < 0 || imageWidth < 0 ||
+	imageLeft + imageWidth > image.width()) {
+	cerr << "ScrollableImageCache::drawImage: ERROR: Source area (left = "
+	     << imageLeft << ", width = " << imageWidth << ", so right = "
+             << imageLeft + imageWidth << ") out of bounds for image of "
+	     << "width " << image.width() << endl;
+	throw std::logic_error("Source area out of bounds in ScrollableImageCache::drawImage");
+    }
+	
+    QPainter painter(&m_image);
+    painter.drawImage(QRect(left, 0, width, m_image.height()),
+		      image,
+		      QRect(imageLeft, 0, imageWidth, image.height()));
+    painter.end();
+
+    if (!isValid()) {
+	m_validLeft = left;
+	m_validWidth = width;
+	return;
+    }
+	
+    if (left < m_validLeft) {
+	if (left + width > m_validLeft + m_validWidth) {
+	    // new image completely contains the old valid area --
+	    // use the new area as is
+	    m_validLeft = left;
+	    m_validWidth = width;
+	} else if (left + width < m_validLeft) {
+	    // new image completely off left of old valid area --
+	    // we can't extend the valid area because the bit in
+	    // between is not valid, so must use the new area only
+	    m_validLeft = left;
+	    m_validWidth = width;
+	} else {
+	    // new image overlaps old valid area on left side --
+	    // use new left edge, and extend width to existing
+	    // right edge
+	    m_validWidth = (m_validLeft + m_validWidth) - left;
+	    m_validLeft = left;
+	}
+    } else {
+	if (left > m_validLeft + m_validWidth) {
+	    // new image completely off right of old valid area --
+	    // we can't extend the valid area because the bit in
+	    // between is not valid, so must use the new area only
+	    m_validLeft = left;
+	    m_validWidth = width;
+	} else if (left + width > m_validLeft + m_validWidth) {
+	    // new image overlaps old valid area on right side --
+	    // use existing left edge, and extend width to new
+	    // right edge
+	    m_validWidth = (left + width) - m_validLeft;
+	    // (m_validLeft unchanged)
+	} else {
+	    // new image completely contained within old valid
+	    // area -- leave the old area unchanged
+	}
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ScrollableImageCache.h	Fri Aug 19 15:58:57 2016 +0100
@@ -0,0 +1,163 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SCROLLABLE_IMAGE_CACHE_H
+#define SCROLLABLE_IMAGE_CACHE_H
+
+#include "base/BaseTypes.h"
+
+#include "LayerGeometryProvider.h"
+
+#include <QImage>
+#include <QRect>
+#include <QPainter>
+
+/**
+ * A cached image for a view that scrolls horizontally, such as a
+ * spectrogram. The cache object holds an image, reports the size of
+ * the image (likely the same as the underlying view, but it's the
+ * caller's responsibility to set the size appropriately), can scroll
+ * the image, and can report and update which contiguous horizontal
+ * range of the image is valid.
+ *
+ * The only way to *update* the valid area in a cache is to draw to it
+ * using the drawImage call.
+ */
+class ScrollableImageCache
+{
+public:
+    ScrollableImageCache() :
+	m_validLeft(0),
+	m_validWidth(0),
+	m_startFrame(0),
+	m_zoomLevel(0)
+    {}
+
+    void invalidate() {
+	m_validWidth = 0;
+    }
+    
+    bool isValid() const {
+	return m_validWidth > 0;
+    }
+
+    QSize getSize() const {
+	return m_image.size();
+    }
+
+    /**
+     * Set the size of the cache. If the new size differs from the
+     * current size, the cache is invalidated.
+     */
+    void resize(QSize newSize) {
+        if (getSize() != newSize) {
+            m_image = QImage(newSize, QImage::Format_ARGB32_Premultiplied);
+            invalidate();
+        }
+    }
+	
+    int getValidLeft() const {
+	return m_validLeft;
+    }
+    
+    int getValidWidth() const {
+	return m_validWidth;
+    }
+
+    int getValidRight() const {
+	return m_validLeft + m_validWidth;
+    }
+
+    QRect getValidArea() const {
+	return QRect(m_validLeft, 0, m_validWidth, m_image.height());
+    }
+    
+    int getZoomLevel() const {
+	return m_zoomLevel;
+    }
+
+    /**
+     * Set the zoom level. If the new zoom level differs from the
+     * current one, the cache is invalidated. (Determining whether to
+     * invalidate the cache here is the only thing the zoom level is
+     * used for.)
+     */
+    void setZoomLevel(int zoom) {
+        if (m_zoomLevel != zoom) {
+            m_zoomLevel = zoom;
+            invalidate();
+        }
+    }
+
+    sv_frame_t getStartFrame() const {
+	return m_startFrame;
+    }
+
+    /**
+     * Set the start frame. If the new start frame differs from the
+     * current one, the cache is invalidated. To scroll, i.e. to set
+     * the start frame while retaining cache validity where possible,
+     * use scrollTo() instead.
+     */
+    void setStartFrame(sv_frame_t frame) {
+        if (m_startFrame != frame) {
+            m_startFrame = frame;
+            invalidate();
+        }
+    }
+    
+    const QImage &getImage() const {
+	return m_image;
+    }
+
+    /**
+     * Set the new start frame for the cache, according to the
+     * geometry of the supplied LayerGeometryProvider, if possible
+     * also moving along any existing valid data within the cache so
+     * that it continues to be valid for the new start frame.
+     */
+    void scrollTo(const LayerGeometryProvider *v, sv_frame_t newStartFrame);
+
+    /**
+     * Take a left coordinate and width describing a region, and
+     * adjust them so that they are contiguous with the cache valid
+     * region and so that the union of the adjusted region with the
+     * cache valid region contains the supplied region.  Does not
+     * modify anything about the cache, only about the arguments.
+     */
+    void adjustToTouchValidArea(int &left, int &width,
+				bool &isLeftOfValidArea) const;
+    
+    /**
+     * Draw from an image onto the cache. The supplied image must have
+     * the same height as the cache and the full height is always
+     * drawn. The left and width parameters determine the target
+     * region of the cache, the imageLeft and imageWidth parameters
+     * the source region of the image.
+     */
+    void drawImage(int left,
+		   int width,
+		   QImage image,
+		   int imageLeft,
+		   int imageWidth);
+    
+private:
+    QImage m_image;
+    int m_validLeft;
+    int m_validWidth;
+    sv_frame_t m_startFrame;
+    int m_zoomLevel;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ScrollableMagRangeCache.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -0,0 +1,113 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "ScrollableMagRangeCache.h"
+
+#include <iostream>
+using namespace std;
+
+//#define DEBUG_SCROLLABLE_MAG_RANGE_CACHE 1
+
+void
+ScrollableMagRangeCache::scrollTo(const LayerGeometryProvider *v,
+				  sv_frame_t newStartFrame)
+{	
+    int dx = (v->getXForFrame(m_startFrame) -
+	      v->getXForFrame(newStartFrame));
+
+#ifdef DEBUG_SCROLLABLE_MAG_RANGE_CACHE
+    cerr << "ScrollableMagRangeCache::scrollTo: start frame " << m_startFrame
+	 << " -> " << newStartFrame << ", dx = " << dx << endl;
+#endif
+
+    if (m_startFrame == newStartFrame) {
+	// haven't moved
+        return;
+    }
+    
+    m_startFrame = newStartFrame;
+
+    if (dx == 0) {
+	// haven't moved visibly (even though start frame may have changed)
+	return;
+    }
+	
+    int w = int(m_ranges.size());
+
+    if (dx <= -w || dx >= w) {
+	// scrolled entirely off
+	invalidate();
+	return;
+    }
+	
+    // dx is in range, cache is scrollable
+
+    if (dx < 0) {
+	// The new start frame is to the left of the old start
+	// frame. We need to add some empty ranges at the left (start)
+	// end and clip the right end. Assemble -dx new values, then
+	// w+dx old values starting at index 0.
+
+	auto newRanges = vector<MagnitudeRange>(-dx);
+	newRanges.insert(newRanges.end(),
+			 m_ranges.begin(), m_ranges.begin() + (w + dx));
+	m_ranges = newRanges;
+	
+    } else {
+	// The new start frame is to the right of the old start
+	// frame. We want to clip the left (start) end and add some
+	// empty ranges at the right end. Assemble w-dx old values
+	// starting at index dx, then dx new values.
+
+	auto newRanges = vector<MagnitudeRange>(dx);
+	newRanges.insert(newRanges.begin(),
+			 m_ranges.begin() + dx, m_ranges.end());
+	m_ranges = newRanges;
+    }
+
+#ifdef DEBUG_SCROLLABLE_MAG_RANGE_CACHE
+    cerr << "maxes (" << m_ranges.size() << ") now: ";
+    for (int i = 0; in_range_for(m_ranges, i); ++i) {
+	cerr << m_ranges[i].getMax() << " ";
+    }
+    cerr << endl;
+#endif
+}
+
+MagnitudeRange
+ScrollableMagRangeCache::getRange(int x, int count) const
+{
+    MagnitudeRange r;
+#ifdef DEBUG_SCROLLABLE_MAG_RANGE_CACHE
+    cerr << "ScrollableMagRangeCache::getRange(" << x << ", " << count << ")" << endl;
+#endif
+    for (int i = 0; i < count; ++i) {
+	r.sample(m_ranges.at(x + i));
+    }
+    return r;
+}
+
+void
+ScrollableMagRangeCache::sampleColumn(int column, const MagnitudeRange &r)
+{
+    if (!in_range_for(m_ranges, column)) {
+	cerr << "ERROR: ScrollableMagRangeCache::sampleColumn: column " << column
+	     << " is out of range for cache of width " << m_ranges.size()
+	     << " (with start frame " << m_startFrame << ")" << endl;
+	throw logic_error("column out of range");
+    } else {
+	m_ranges[column].sample(r);
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ScrollableMagRangeCache.h	Fri Aug 19 15:58:57 2016 +0100
@@ -0,0 +1,139 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SCROLLABLE_MAG_RANGE_CACHE_H
+#define SCROLLABLE_MAG_RANGE_CACHE_H
+
+#include "base/BaseTypes.h"
+#include "base/MagnitudeRange.h"
+
+#include "LayerGeometryProvider.h"
+
+/**
+ * A cached set of magnitude range records for a view that scrolls
+ * horizontally, such as a spectrogram. The cache object holds a
+ * magnitude range per column of the view, can report width (likely
+ * the same as the underlying view, but it's the caller's
+ * responsibility to set the size appropriately), can scroll the set
+ * of ranges, and can report and update which columns have had a range
+ * specified.
+ *
+ * The only way to *update* the valid area in a cache is to update the
+ * magnitude range for a column using the sampleColumn call.
+ */
+class ScrollableMagRangeCache
+{
+public:
+    ScrollableMagRangeCache() :
+	m_startFrame(0),
+	m_zoomLevel(0)
+    {}
+
+    void invalidate() {
+	m_ranges = std::vector<MagnitudeRange>(m_ranges.size());
+    }
+    
+    int getWidth() const {
+	return int(m_ranges.size());
+    }
+
+    /**
+     * Set the width of the cache in columns. If the new size differs
+     * from the current size, the cache is invalidated.
+     */
+    void resize(int newWidth) {
+        if (getWidth() != newWidth) {
+	    m_ranges = std::vector<MagnitudeRange>(newWidth);
+        }
+    }
+	
+    int getZoomLevel() const {
+	return m_zoomLevel;
+    }
+
+    /**
+     * Set the zoom level. If the new zoom level differs from the
+     * current one, the cache is invalidated. (Determining whether to
+     * invalidate the cache here is the only thing the zoom level is
+     * used for.)
+     */
+    void setZoomLevel(int zoom) {
+        if (m_zoomLevel != zoom) {
+            m_zoomLevel = zoom;
+            invalidate();
+        }
+    }
+
+    sv_frame_t getStartFrame() const {
+	return m_startFrame;
+    }
+
+    /**
+     * Set the start frame. If the new start frame differs from the
+     * current one, the cache is invalidated. To scroll, i.e. to set
+     * the start frame while retaining cache validity where possible,
+     * use scrollTo() instead.
+     */
+    void setStartFrame(sv_frame_t frame) {
+        if (m_startFrame != frame) {
+            m_startFrame = frame;
+            invalidate();
+        }
+    }
+
+    bool isColumnSet(int column) const {
+	return in_range_for(m_ranges, column) && m_ranges.at(column).isSet();
+    }
+
+    bool areColumnsSet(int x, int count) const {
+	for (int i = 0; i < count; ++i) {
+	    if (!isColumnSet(x + i)) return false;
+	}
+	return true;
+    }
+    
+    /**
+     * Get the magnitude range for a single column.
+     */
+    MagnitudeRange getRange(int column) const {
+	return m_ranges.at(column);
+    }
+
+    /**
+     * Get the magnitude range for a range of columns.
+     */
+    MagnitudeRange getRange(int x, int count) const;
+    
+    /**
+     * Set the new start frame for the cache, according to the
+     * geometry of the supplied LayerGeometryProvider, if possible
+     * also moving along any existing valid data within the cache so
+     * that it continues to be valid for the new start frame.
+     */
+    void scrollTo(const LayerGeometryProvider *v, sv_frame_t newStartFrame);
+    
+    /**
+     * Update a column in the cache, by column index. (Column zero is
+     * the first column in the cache, it has nothing to do with any
+     * underlying model that the cache may be used with.)
+     */
+    void sampleColumn(int column, const MagnitudeRange &r);
+    
+private:
+    std::vector<MagnitudeRange> m_ranges;
+    sv_frame_t m_startFrame;
+    int m_zoomLevel;
+};
+
+#endif
--- a/layer/SliceLayer.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/SliceLayer.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -485,17 +485,17 @@
         v->drawVisibleText
             (paint, xorigin + 5,
              paint.fontMetrics().ascent() + 5,
-             startText, View::OutlinedText);
+             startText, PaintAssistant::OutlinedText);
         
         v->drawVisibleText
             (paint, xorigin + 5,
              paint.fontMetrics().ascent() + paint.fontMetrics().height() + 10,
-             endText, View::OutlinedText);
+             endText, PaintAssistant::OutlinedText);
         
         v->drawVisibleText
             (paint, xorigin + 5,
              paint.fontMetrics().ascent() + 2*paint.fontMetrics().height() + 15,
-             durationText, View::OutlinedText);
+             durationText, PaintAssistant::OutlinedText);
     }
 */
 }
--- a/layer/SpectrogramLayer.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/SpectrogramLayer.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -23,21 +23,25 @@
 #include "base/Preferences.h"
 #include "base/RangeMapper.h"
 #include "base/LogRange.h"
+#include "base/ColumnOp.h"
+#include "base/Strings.h"
 #include "widgets/CommandHistory.h"
+#include "data/model/Dense3DModelPeakCache.h"
+
 #include "ColourMapper.h"
-#include "ImageRegionFinder.h"
-#include "data/model/Dense3DModelPeakCache.h"
 #include "PianoScale.h"
+#include "PaintAssistant.h"
+#include "Colour3DPlotRenderer.h"
 
 #include <QPainter>
 #include <QImage>
 #include <QPixmap>
 #include <QRect>
-#include <QTimer>
 #include <QApplication>
 #include <QMessageBox>
 #include <QMouseEvent>
 #include <QTextStream>
+#include <QSettings>
 
 #include <iostream>
 
@@ -48,9 +52,10 @@
 #include <alloca.h>
 #endif
 
+//#define DEBUG_SPECTROGRAM 1
 //#define DEBUG_SPECTROGRAM_REPAINT 1
 
-using std::vector;
+using namespace std;
 
 SpectrogramLayer::SpectrogramLayer(Configuration config) :
     m_model(0),
@@ -58,29 +63,33 @@
     m_windowSize(1024),
     m_windowType(HanningWindow),
     m_windowHopLevel(2),
-    m_zeroPadLevel(0),
-    m_fftSize(1024),
     m_gain(1.0),
     m_initialGain(1.0),
-    m_threshold(0.0),
-    m_initialThreshold(0.0),
+    m_threshold(1.0e-8f),
+    m_initialThreshold(1.0e-8f),
     m_colourRotation(0),
     m_initialRotation(0),
     m_minFrequency(10),
     m_maxFrequency(8000),
     m_initialMaxFrequency(8000),
-    m_colourScale(dBColourScale),
+    m_colourScale(ColourScaleType::Log),
+    m_colourScaleMultiple(1.0),
     m_colourMap(0),
-    m_frequencyScale(LinearFrequencyScale),
-    m_binDisplay(AllBins),
-    m_normalization(NoNormalization),
+    m_binScale(BinScale::Linear),
+    m_binDisplay(BinDisplay::AllBins),
+    m_normalization(ColumnNormalization::None),
+    m_normalizeVisibleArea(false),
     m_lastEmittedZoomStep(-1),
     m_synchronous(false),
     m_haveDetailedScale(false),
-    m_lastPaintBlockWidth(0),
     m_exiting(false),
-    m_sliceableModel(0)
+    m_fftModel(0),
+    m_peakCache(0),
+    m_peakCacheDivisor(8)
 {
+    QString colourConfigName = "spectrogram-colour";
+    int colourConfigDefault = int(ColourMapper::Green);
+    
     if (config == FullRangeDb) {
         m_initialMaxFrequency = 0;
         setMaxFrequency(0);
@@ -90,9 +99,11 @@
         m_initialMaxFrequency = 1500;
 	setMaxFrequency(1500);
         setMinFrequency(40);
-	setColourScale(LinearColourScale);
+	setColourScale(ColourScaleType::Linear);
         setColourMap(ColourMapper::Sunset);
-        setFrequencyScale(LogFrequencyScale);
+        setBinScale(BinScale::Log);
+        colourConfigName = "spectrogram-melodic-colour";
+        colourConfigDefault = int(ColourMapper::Sunset);
 //        setGain(20);
     } else if (config == MelodicPeaks) {
 	setWindowSize(4096);
@@ -100,23 +111,82 @@
         m_initialMaxFrequency = 2000;
 	setMaxFrequency(2000);
 	setMinFrequency(40);
-	setFrequencyScale(LogFrequencyScale);
-	setColourScale(LinearColourScale);
-	setBinDisplay(PeakFrequencies);
-        setNormalization(NormalizeColumns);
+	setBinScale(BinScale::Log);
+	setColourScale(ColourScaleType::Linear);
+	setBinDisplay(BinDisplay::PeakFrequencies);
+        setNormalization(ColumnNormalization::Max1);
+        colourConfigName = "spectrogram-melodic-colour";
+        colourConfigDefault = int(ColourMapper::Sunset);
     }
 
+    QSettings settings;
+    settings.beginGroup("Preferences");
+    setColourMap(settings.value(colourConfigName, colourConfigDefault).toInt());
+    settings.endGroup();
+    
     Preferences *prefs = Preferences::getInstance();
     connect(prefs, SIGNAL(propertyChanged(PropertyContainer::PropertyName)),
             this, SLOT(preferenceChanged(PropertyContainer::PropertyName)));
     setWindowType(prefs->getWindowType());
-
-    initialisePalette();
 }
 
 SpectrogramLayer::~SpectrogramLayer()
 {
-    invalidateFFTModels();
+    invalidateRenderers();
+    invalidateFFTModel();
+}
+
+pair<ColourScaleType, double>
+SpectrogramLayer::convertToColourScale(int value)
+{
+    switch (value) {
+    case 0: return { ColourScaleType::Linear, 1.0 };
+    case 1: return { ColourScaleType::Meter, 1.0 };
+    case 2: return { ColourScaleType::Log, 2.0 }; // dB^2 (i.e. log of power)
+    case 3: return { ColourScaleType::Log, 1.0 }; // dB   (of magnitude)
+    case 4: return { ColourScaleType::Phase, 1.0 };
+    default: return { ColourScaleType::Linear, 1.0 };
+    }
+}
+
+int
+SpectrogramLayer::convertFromColourScale(ColourScaleType scale, double multiple)
+{
+    switch (scale) {
+    case ColourScaleType::Linear: return 0;
+    case ColourScaleType::Meter: return 1;
+    case ColourScaleType::Log: return (multiple > 1.5 ? 2 : 3);
+    case ColourScaleType::Phase: return 4;
+    case ColourScaleType::PlusMinusOne:
+    case ColourScaleType::Absolute:
+    default: return 0;
+    }
+}
+
+std::pair<ColumnNormalization, bool>
+SpectrogramLayer::convertToColumnNorm(int value)
+{
+    switch (value) {
+    default:
+    case 0: return { ColumnNormalization::None, false };
+    case 1: return { ColumnNormalization::Max1, false };
+    case 2: return { ColumnNormalization::None, true }; // visible area
+    case 3: return { ColumnNormalization::Hybrid, false };
+    }
+}
+
+int
+SpectrogramLayer::convertFromColumnNorm(ColumnNormalization norm, bool visible)
+{
+    if (visible) return 2;
+    switch (norm) {
+    case ColumnNormalization::None: return 0;
+    case ColumnNormalization::Max1: return 1;
+    case ColumnNormalization::Hybrid: return 3;
+
+    case ColumnNormalization::Sum1:
+    default: return 0;
+    }
 }
 
 void
@@ -127,7 +197,7 @@
     if (model == m_model) return;
 
     m_model = model;
-    invalidateFFTModels();
+    invalidateFFTModel();
 
     if (!m_model || !m_model->isOK()) return;
 
@@ -156,7 +226,6 @@
 //    list.push_back("Min Frequency");
 //    list.push_back("Max Frequency");
     list.push_back("Frequency Scale");
-////    list.push_back("Zero Padding");
     return list;
 }
 
@@ -175,7 +244,6 @@
     if (name == "Min Frequency") return tr("Min Frequency");
     if (name == "Max Frequency") return tr("Max Frequency");
     if (name == "Frequency Scale") return tr("Frequency Scale");
-    if (name == "Zero Padding") return tr("Smoothing");
     return "";
 }
 
@@ -191,7 +259,6 @@
     if (name == "Gain") return RangeProperty;
     if (name == "Colour Rotation") return RangeProperty;
     if (name == "Threshold") return RangeProperty;
-    if (name == "Zero Padding") return ToggleProperty;
     return ValueProperty;
 }
 
@@ -201,8 +268,7 @@
     if (name == "Bin Display" ||
         name == "Frequency Scale") return tr("Bins");
     if (name == "Window Size" ||
-	name == "Window Increment" ||
-        name == "Zero Padding") return tr("Window");
+	name == "Window Increment") return tr("Window");
     if (name == "Colour" ||
 	name == "Threshold" ||
 	name == "Colour Rotation") return tr("Colour");
@@ -238,8 +304,8 @@
 
     } else if (name == "Threshold") {
 
-	*min = -50;
-	*max = 0;
+	*min = -81;
+	*max = -1;
 
         *deflt = int(lrint(AudioLevel::multiplier_to_dB(m_initialThreshold)));
 	if (*deflt < *min) *deflt = *min;
@@ -259,11 +325,12 @@
 
     } else if (name == "Colour Scale") {
 
+        // linear, meter, db^2, db, phase
 	*min = 0;
 	*max = 4;
-        *deflt = int(dBColourScale);
-
-	val = (int)m_colourScale;
+        *deflt = 2;
+
+	val = convertFromColourScale(m_colourScale, m_colourScaleMultiple);
 
     } else if (name == "Colour") {
 
@@ -291,14 +358,6 @@
 
         val = m_windowHopLevel;
     
-    } else if (name == "Zero Padding") {
-	
-	*min = 0;
-	*max = 1;
-        *deflt = 0;
-	
-        val = m_zeroPadLevel > 0 ? 1 : 0;
-    
     } else if (name == "Min Frequency") {
 
 	*min = 0;
@@ -341,22 +400,23 @@
 
 	*min = 0;
 	*max = 1;
-        *deflt = int(LinearFrequencyScale);
-	val = (int)m_frequencyScale;
+        *deflt = int(BinScale::Linear);
+	val = (int)m_binScale;
 
     } else if (name == "Bin Display") {
 
 	*min = 0;
 	*max = 2;
-        *deflt = int(AllBins);
+        *deflt = int(BinDisplay::AllBins);
 	val = (int)m_binDisplay;
 
     } else if (name == "Normalization") {
 	
         *min = 0;
         *max = 3;
-        *deflt = int(NoNormalization);
-        val = (int)m_normalization;
+        *deflt = 0;
+        
+        val = convertFromColumnNorm(m_normalization, m_normalizeVisibleArea);
 
     } else {
 	val = Layer::getPropertyRangeAndValue(name, min, max, deflt);
@@ -399,10 +459,6 @@
 	case 5: return tr("93.75 %");
 	}
     }
-    if (name == "Zero Padding") {
-        if (value == 0) return tr("None");
-        return QString("%1x").arg(value + 1);
-    }
     if (name == "Min Frequency") {
 	switch (value) {
 	default:
@@ -474,7 +530,8 @@
         return new LinearRangeMapper(-50, 50, -25, 25, tr("dB"));
     }
     if (name == "Threshold") {
-        return new LinearRangeMapper(-50, 0, -50, 0, tr("dB"));
+        return new LinearRangeMapper(-81, -1, -81, -1, tr("dB"), false,
+                                     { { -81, Strings::minus_infinity } });
     }
     return 0;
 }
@@ -485,7 +542,7 @@
     if (name == "Gain") {
 	setGain(float(pow(10, float(value)/20.0)));
     } else if (name == "Threshold") {
-	if (value == -50) setThreshold(0.0);
+	if (value == -81) setThreshold(0.0);
 	else setThreshold(float(AudioLevel::dB_to_multiplier(value)));
     } else if (name == "Colour Rotation") {
 	setColourRotation(value);
@@ -495,8 +552,6 @@
 	setWindowSize(32 << value);
     } else if (name == "Window Increment") {
         setWindowHopLevel(value);
-    } else if (name == "Zero Padding") {
-        setZeroPadLevel(value > 0.1 ? 3 : 0);
     } else if (name == "Min Frequency") {
 	switch (value) {
 	default:
@@ -536,112 +591,50 @@
             m_lastEmittedZoomStep = vs;
         }
     } else if (name == "Colour Scale") {
+        setColourScaleMultiple(1.0);
 	switch (value) {
 	default:
-	case 0: setColourScale(LinearColourScale); break;
-	case 1: setColourScale(MeterColourScale); break;
-	case 2: setColourScale(dBSquaredColourScale); break;
-	case 3: setColourScale(dBColourScale); break;
-	case 4: setColourScale(PhaseColourScale); break;
+	case 0: setColourScale(ColourScaleType::Linear); break;
+	case 1: setColourScale(ColourScaleType::Meter); break;
+	case 2:
+            setColourScale(ColourScaleType::Log);
+            setColourScaleMultiple(2.0);
+            break;
+	case 3: setColourScale(ColourScaleType::Log); break;
+	case 4: setColourScale(ColourScaleType::Phase); break;
 	}
     } else if (name == "Frequency Scale") {
 	switch (value) {
 	default:
-	case 0: setFrequencyScale(LinearFrequencyScale); break;
-	case 1: setFrequencyScale(LogFrequencyScale); break;
+	case 0: setBinScale(BinScale::Linear); break;
+	case 1: setBinScale(BinScale::Log); break;
 	}
     } else if (name == "Bin Display") {
 	switch (value) {
 	default:
-	case 0: setBinDisplay(AllBins); break;
-	case 1: setBinDisplay(PeakBins); break;
-	case 2: setBinDisplay(PeakFrequencies); break;
+	case 0: setBinDisplay(BinDisplay::AllBins); break;
+	case 1: setBinDisplay(BinDisplay::PeakBins); break;
+	case 2: setBinDisplay(BinDisplay::PeakFrequencies); break;
 	}
     } else if (name == "Normalization") {
-        switch (value) {
-        default:
-        case 0: setNormalization(NoNormalization); break;
-        case 1: setNormalization(NormalizeColumns); break;
-        case 2: setNormalization(NormalizeVisibleArea); break;
-        case 3: setNormalization(NormalizeHybrid); break;
-        }
+        auto n = convertToColumnNorm(value);
+        setNormalization(n.first);
+        setNormalizeVisibleArea(n.second);
     }
 }
 
 void
-SpectrogramLayer::invalidateImageCaches()
+SpectrogramLayer::invalidateRenderers()
 {
-    for (ViewImageCache::iterator i = m_imageCaches.begin();
-         i != m_imageCaches.end(); ++i) {
-        i->second.validArea = QRect();
+#ifdef DEBUG_SPECTROGRAM
+    cerr << "SpectrogramLayer::invalidateRenderers called" << endl;
+#endif
+
+    for (ViewRendererMap::iterator i = m_renderers.begin();
+         i != m_renderers.end(); ++i) {
+        delete i->second;
     }
-}
-
-void
-SpectrogramLayer::invalidateImageCaches(sv_frame_t startFrame, sv_frame_t endFrame)
-{
-    for (ViewImageCache::iterator i = m_imageCaches.begin();
-         i != m_imageCaches.end(); ++i) {
-
-        //!!! when are views removed from the map? on setLayerDormant?
-        const LayerGeometryProvider *v = i->first;
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "SpectrogramLayer::invalidateImageCaches(" 
-                  << startFrame << ", " << endFrame << "): view range is "
-                  << v->getStartFrame() << ", " << v->getEndFrame()
-                  << endl;
-
-        cerr << "Valid area was: " << i->second.validArea.x() << ", "
-                  << i->second.validArea.y() << " "
-                  << i->second.validArea.width() << "x"
-                  << i->second.validArea.height() << endl;
-#endif
-
-        if (int(startFrame) > v->getStartFrame()) {
-            if (startFrame >= v->getEndFrame()) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                cerr << "Modified start frame is off right of view" << endl;
-#endif
-                return;
-            }
-            int x = v->getXForFrame(startFrame);
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "clipping from 0 to " << x-1 << endl;
-#endif
-            if (x > 1) {
-                i->second.validArea &=
-                    QRect(0, 0, x-1, v->getPaintHeight());
-            } else {
-                i->second.validArea = QRect();
-            }
-        } else {
-            if (int(endFrame) < v->getStartFrame()) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                cerr << "Modified end frame is off left of view" << endl;
-#endif
-                return;
-            }
-            int x = v->getXForFrame(endFrame);
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "clipping from " << x+1 << " to " << v->getPaintWidth()
-                      << endl;
-#endif
-            if (x < v->getPaintWidth()) {
-                i->second.validArea &=
-                    QRect(x+1, 0, v->getPaintWidth()-(x+1), v->getPaintHeight());
-            } else {
-                i->second.validArea = QRect();
-            }
-        }
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "Valid area is now: " << i->second.validArea.x() << ", "
-                  << i->second.validArea.y() << " "
-                  << i->second.validArea.width() << "x"
-                  << i->second.validArea.height() << endl;
-#endif
-    }
+    m_renderers.clear();
 }
 
 void
@@ -654,12 +647,13 @@
         return;
     }
     if (name == "Spectrogram Y Smoothing") {
-        invalidateImageCaches();
+        setWindowSize(m_windowSize);
+        invalidateRenderers();
         invalidateMagnitudes();
         emit layerParametersChanged();
     }
     if (name == "Spectrogram X Smoothing") {
-        invalidateImageCaches();
+        invalidateRenderers();
         invalidateMagnitudes();
         emit layerParametersChanged();
     }
@@ -673,9 +667,9 @@
 {
     if (m_channel == ch) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     m_channel = ch;
-    invalidateFFTModels();
+    invalidateFFTModel();
 
     emit layerParametersChanged();
 }
@@ -686,17 +680,40 @@
     return m_channel;
 }
 
+int
+SpectrogramLayer::getFFTOversampling() const
+{
+    if (m_binDisplay != BinDisplay::AllBins) {
+        return 1;
+    }
+
+    Preferences::SpectrogramSmoothing smoothing = 
+        Preferences::getInstance()->getSpectrogramSmoothing();
+    
+    if (smoothing == Preferences::NoSpectrogramSmoothing ||
+        smoothing == Preferences::SpectrogramInterpolated) {
+        return 1;
+    }
+
+    return 4;
+}
+
+int
+SpectrogramLayer::getFFTSize() const
+{
+    return m_windowSize * getFFTOversampling();
+}
+
 void
 SpectrogramLayer::setWindowSize(int ws)
 {
     if (m_windowSize == ws) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_windowSize = ws;
-    m_fftSize = ws * (m_zeroPadLevel + 1);
     
-    invalidateFFTModels();
+    invalidateFFTModel();
 
     emit layerParametersChanged();
 }
@@ -712,11 +729,11 @@
 {
     if (m_windowHopLevel == v) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_windowHopLevel = v;
     
-    invalidateFFTModels();
+    invalidateFFTModel();
 
     emit layerParametersChanged();
 
@@ -730,36 +747,15 @@
 }
 
 void
-SpectrogramLayer::setZeroPadLevel(int v)
-{
-    if (m_zeroPadLevel == v) return;
-
-    invalidateImageCaches();
-    
-    m_zeroPadLevel = v;
-    m_fftSize = m_windowSize * (v + 1);
-
-    invalidateFFTModels();
-
-    emit layerParametersChanged();
-}
-
-int
-SpectrogramLayer::getZeroPadLevel() const
-{
-    return m_zeroPadLevel;
-}
-
-void
 SpectrogramLayer::setWindowType(WindowType w)
 {
     if (m_windowType == w) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_windowType = w;
 
-    invalidateFFTModels();
+    invalidateFFTModel();
 
     emit layerParametersChanged();
 }
@@ -778,7 +774,7 @@
 
     if (m_gain == gain) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_gain = gain;
     
@@ -796,7 +792,7 @@
 {
     if (m_threshold == threshold) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_threshold = threshold;
 
@@ -816,7 +812,7 @@
 
 //    SVDEBUG << "SpectrogramLayer::setMinFrequency: " << mf << endl;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     invalidateMagnitudes();
     
     m_minFrequency = mf;
@@ -837,7 +833,7 @@
 
 //    SVDEBUG << "SpectrogramLayer::setMaxFrequency: " << mf << endl;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     invalidateMagnitudes();
     
     m_maxFrequency = mf;
@@ -854,47 +850,67 @@
 void
 SpectrogramLayer::setColourRotation(int r)
 {
-    invalidateImageCaches();
-
     if (r < 0) r = 0;
     if (r > 256) r = 256;
     int distance = r - m_colourRotation;
 
     if (distance != 0) {
-	rotatePalette(-distance);
 	m_colourRotation = r;
     }
+
+    // Initially the idea with colour rotation was that we would just
+    // rotate the palette of an already-generated cache. That's not
+    // really practical now that cacheing is handled in a separate
+    // class in which the main cache no longer has a palette.
+    invalidateRenderers();
     
     emit layerParametersChanged();
 }
 
 void
-SpectrogramLayer::setColourScale(ColourScale colourScale)
+SpectrogramLayer::setColourScale(ColourScaleType colourScale)
 {
     if (m_colourScale == colourScale) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_colourScale = colourScale;
     
     emit layerParametersChanged();
 }
 
-SpectrogramLayer::ColourScale
+ColourScaleType
 SpectrogramLayer::getColourScale() const
 {
     return m_colourScale;
 }
 
 void
+SpectrogramLayer::setColourScaleMultiple(double multiple)
+{
+    if (m_colourScaleMultiple == multiple) return;
+
+    invalidateRenderers();
+    
+    m_colourScaleMultiple = multiple;
+    
+    emit layerParametersChanged();
+}
+
+double
+SpectrogramLayer::getColourScaleMultiple() const
+{
+    return m_colourScaleMultiple;
+}
+
+void
 SpectrogramLayer::setColourMap(int map)
 {
     if (m_colourMap == map) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     
     m_colourMap = map;
-    initialisePalette();
 
     emit layerParametersChanged();
 }
@@ -906,20 +922,20 @@
 }
 
 void
-SpectrogramLayer::setFrequencyScale(FrequencyScale frequencyScale)
+SpectrogramLayer::setBinScale(BinScale binScale)
 {
-    if (m_frequencyScale == frequencyScale) return;
-
-    invalidateImageCaches();
-    m_frequencyScale = frequencyScale;
+    if (m_binScale == binScale) return;
+
+    invalidateRenderers();
+    m_binScale = binScale;
 
     emit layerParametersChanged();
 }
 
-SpectrogramLayer::FrequencyScale
-SpectrogramLayer::getFrequencyScale() const
+BinScale
+SpectrogramLayer::getBinScale() const
 {
-    return m_frequencyScale;
+    return m_binScale;
 }
 
 void
@@ -927,37 +943,55 @@
 {
     if (m_binDisplay == binDisplay) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     m_binDisplay = binDisplay;
 
     emit layerParametersChanged();
 }
 
-SpectrogramLayer::BinDisplay
+BinDisplay
 SpectrogramLayer::getBinDisplay() const
 {
     return m_binDisplay;
 }
 
 void
-SpectrogramLayer::setNormalization(Normalization n)
+SpectrogramLayer::setNormalization(ColumnNormalization n)
 {
     if (m_normalization == n) return;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     invalidateMagnitudes();
     m_normalization = n;
 
     emit layerParametersChanged();
 }
 
-SpectrogramLayer::Normalization
+ColumnNormalization
 SpectrogramLayer::getNormalization() const
 {
     return m_normalization;
 }
 
 void
+SpectrogramLayer::setNormalizeVisibleArea(bool n)
+{
+    if (m_normalizeVisibleArea == n) return;
+
+    invalidateRenderers();
+    invalidateMagnitudes();
+    m_normalizeVisibleArea = n;
+    
+    emit layerParametersChanged();
+}
+
+bool
+SpectrogramLayer::getNormalizeVisibleArea() const
+{
+    return m_normalizeVisibleArea;
+}
+
+void
 SpectrogramLayer::setLayerDormant(const LayerGeometryProvider *v, bool dormant)
 {
     if (dormant) {
@@ -973,33 +1007,7 @@
 
         Layer::setLayerDormant(v, true);
 
-        const View *view = v->getView();
-        
-	invalidateImageCaches();
-
-        m_imageCaches.erase(view);
-
-        if (m_fftModels.find(view) != m_fftModels.end()) {
-
-            if (m_sliceableModel == m_fftModels[view]) {
-                bool replaced = false;
-                for (ViewFFTMap::iterator i = m_fftModels.begin();
-                     i != m_fftModels.end(); ++i) {
-                    if (i->second != m_sliceableModel) {
-                        emit sliceableModelReplaced(m_sliceableModel, i->second);
-                        replaced = true;
-                        break;
-                    }
-                }
-                if (!replaced) emit sliceableModelReplaced(m_sliceableModel, 0);
-            }
-
-            delete m_fftModels[view];
-            m_fftModels.erase(view);
-
-            delete m_peakCaches[view];
-            m_peakCaches.erase(view);
-        }
+	invalidateRenderers();
 	
     } else {
 
@@ -1014,18 +1022,30 @@
     cerr << "SpectrogramLayer::cacheInvalid()" << endl;
 #endif
 
-    invalidateImageCaches();
+    invalidateRenderers();
     invalidateMagnitudes();
 }
 
 void
-SpectrogramLayer::cacheInvalid(sv_frame_t from, sv_frame_t to)
+SpectrogramLayer::cacheInvalid(
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+    sv_frame_t from, sv_frame_t to
+#else 
+    sv_frame_t     , sv_frame_t
+#endif
+    )
 {
 #ifdef DEBUG_SPECTROGRAM_REPAINT
     cerr << "SpectrogramLayer::cacheInvalid(" << from << ", " << to << ")" << endl;
 #endif
 
-    invalidateImageCaches(from, to);
+    // We used to call invalidateMagnitudes(from, to) to invalidate
+    // only those caches whose views contained some of the (from, to)
+    // range. That's the right thing to do; it has been lost in
+    // pulling out the image cache code, but it might not matter very
+    // much, since the underlying models for spectrogram layers don't
+    // change very often. Let's see.
+    invalidateRenderers();
     invalidateMagnitudes();
 }
 
@@ -1035,146 +1055,16 @@
     return ColourMapper(m_colourMap, 1.f, 255.f).hasLightBackground();
 }
 
-void
-SpectrogramLayer::initialisePalette()
-{
-    int formerRotation = m_colourRotation;
-
-    if (m_colourMap == (int)ColourMapper::BlackOnWhite) {
-	m_palette.setColour(NO_VALUE, Qt::white);
-    } else {
-	m_palette.setColour(NO_VALUE, Qt::black);
-    }
-
-    ColourMapper mapper(m_colourMap, 1.f, 255.f);
-    
-    for (int pixel = 1; pixel < 256; ++pixel) {
-        m_palette.setColour((unsigned char)pixel, mapper.map(pixel));
-    }
-
-    m_crosshairColour = mapper.getContrastingColour();
-
-    m_colourRotation = 0;
-    rotatePalette(m_colourRotation - formerRotation);
-    m_colourRotation = formerRotation;
-
-    m_drawBuffer = QImage();
-}
-
-void
-SpectrogramLayer::rotatePalette(int distance)
-{
-    QColor newPixels[256];
-
-    newPixels[NO_VALUE] = m_palette.getColour(NO_VALUE);
-
-    for (int pixel = 1; pixel < 256; ++pixel) {
-	int target = pixel + distance;
-	while (target < 1) target += 255;
-	while (target > 255) target -= 255;
-	newPixels[target] = m_palette.getColour((unsigned char)pixel);
-    }
-
-    for (int pixel = 0; pixel < 256; ++pixel) {
-	m_palette.setColour((unsigned char)pixel, newPixels[pixel]);
-    }
-
-    m_drawBuffer = QImage();
-}
-
-unsigned char
-SpectrogramLayer::getDisplayValue(LayerGeometryProvider *v, double input) const
-{
-    int value;
-
-    double min = 0.0;
-    double max = 1.0;
-
-    if (m_normalization == NormalizeVisibleArea) {
-        min = m_viewMags[v].getMin();
-        max = m_viewMags[v].getMax();
-    } else if (m_normalization != NormalizeColumns) {
-        if (m_colourScale == LinearColourScale //||
-//            m_colourScale == MeterColourScale) {
-            ) {
-            max = 0.1;
-        }
-    }
-
-    double thresh = -80.0;
-
-    if (max == 0.0) max = 1.0;
-    if (max == min) min = max - 0.0001;
-
-    switch (m_colourScale) {
-	
-    default:
-    case LinearColourScale:
-        value = int(((input - min) / (max - min)) * 255.0) + 1;
-	break;
-	
-    case MeterColourScale:
-        value = AudioLevel::multiplier_to_preview
-            ((input - min) / (max - min), 254) + 1;
-	break;
-
-    case dBSquaredColourScale:
-        input = ((input - min) * (input - min)) / ((max - min) * (max - min));
-        if (input > 0.0) {
-            input = 10.0 * log10(input);
-        } else {
-            input = thresh;
-        }
-        if (min > 0.0) {
-            thresh = 10.0 * log10(min * min);
-            if (thresh < -80.0) thresh = -80.0;
-        }
-	input = (input - thresh) / (-thresh);
-	if (input < 0.0) input = 0.0;
-	if (input > 1.0) input = 1.0;
-	value = int(input * 255.0) + 1;
-	break;
-	
-    case dBColourScale:
-        //!!! experiment with normalizing the visible area this way.
-        //In any case, we need to have some indication of what the dB
-        //scale is relative to.
-        input = (input - min) / (max - min);
-        if (input > 0.0) {
-            input = 10.0 * log10(input);
-        } else {
-            input = thresh;
-        }
-        if (min > 0.0) {
-            thresh = 10.0 * log10(min);
-            if (thresh < -80.0) thresh = -80.0;
-        }
-	input = (input - thresh) / (-thresh);
-	if (input < 0.0) input = 0.0;
-	if (input > 1.0) input = 1.0;
-	value = int(input * 255.0) + 1;
-	break;
-	
-    case PhaseColourScale:
-	value = int((input * 127.0 / M_PI) + 128);
-	break;
-    }
-
-    if (value > UCHAR_MAX) value = UCHAR_MAX;
-    if (value < 0) value = 0;
-    return (unsigned char)value;
-}
-
 double
 SpectrogramLayer::getEffectiveMinFrequency() const
 {
     sv_samplerate_t sr = m_model->getSampleRate();
-    double minf = double(sr) / m_fftSize;
+    double minf = double(sr) / getFFTSize();
 
     if (m_minFrequency > 0.0) {
-	int minbin = int((double(m_minFrequency) * m_fftSize) / sr + 0.01);
+	int minbin = int((double(m_minFrequency) * getFFTSize()) / sr + 0.01);
 	if (minbin < 1) minbin = 1;
-	minf = minbin * sr / m_fftSize;
+	minf = minbin * sr / getFFTSize();
     }
 
     return minf;
@@ -1187,9 +1077,9 @@
     double maxf = double(sr) / 2;
 
     if (m_maxFrequency > 0.0) {
-	int maxbin = int((double(m_maxFrequency) * m_fftSize) / sr + 0.1);
-	if (maxbin > m_fftSize / 2) maxbin = m_fftSize / 2;
-	maxf = maxbin * sr / m_fftSize;
+	int maxbin = int((double(m_maxFrequency) * getFFTSize()) / sr + 0.1);
+	if (maxbin > getFFTSize() / 2) maxbin = getFFTSize() / 2;
+	maxf = maxbin * sr / getFFTSize();
     }
 
     return maxf;
@@ -1199,55 +1089,46 @@
 SpectrogramLayer::getYBinRange(LayerGeometryProvider *v, int y, double &q0, double &q1) const
 {
     Profiler profiler("SpectrogramLayer::getYBinRange");
-    
     int h = v->getPaintHeight();
     if (y < 0 || y >= h) return false;
-
+    q0 = getBinForY(v, y);
+    q1 = getBinForY(v, y-1);
+    return true;
+}
+
+double
+SpectrogramLayer::getYForBin(const LayerGeometryProvider *v, double bin) const
+{
+    double minf = getEffectiveMinFrequency();
+    double maxf = getEffectiveMaxFrequency();
+    bool logarithmic = (m_binScale == BinScale::Log);
+    sv_samplerate_t sr = m_model->getSampleRate();
+
+    double freq = (bin * sr) / getFFTSize();
+    
+    double y = v->getYForFrequency(freq, minf, maxf, logarithmic);
+    
+    return y;
+}
+
+double
+SpectrogramLayer::getBinForY(const LayerGeometryProvider *v, double y) const
+{
     sv_samplerate_t sr = m_model->getSampleRate();
     double minf = getEffectiveMinFrequency();
     double maxf = getEffectiveMaxFrequency();
 
-    bool logarithmic = (m_frequencyScale == LogFrequencyScale);
-
-    q0 = v->getFrequencyForY(y, minf, maxf, logarithmic);
-    q1 = v->getFrequencyForY(y - 1, minf, maxf, logarithmic);
-
-    // Now map these on to ("proportions of") actual bins, using raw
-    // FFT size (unsmoothed)
-
-    q0 = (q0 * m_fftSize) / sr;
-    q1 = (q1 * m_fftSize) / sr;
-
-    return true;
+    bool logarithmic = (m_binScale == BinScale::Log);
+
+    double freq = v->getFrequencyForY(y, minf, maxf, logarithmic);
+
+    // Now map on to ("proportion of") actual bins
+    double bin = (freq * getFFTSize()) / sr;
+
+    return bin;
 }
 
 bool
-SpectrogramLayer::getSmoothedYBinRange(LayerGeometryProvider *v, int y, double &q0, double &q1) const
-{
-    Profiler profiler("SpectrogramLayer::getSmoothedYBinRange");
-
-    int h = v->getPaintHeight();
-    if (y < 0 || y >= h) return false;
-
-    sv_samplerate_t sr = m_model->getSampleRate();
-    double minf = getEffectiveMinFrequency();
-    double maxf = getEffectiveMaxFrequency();
-
-    bool logarithmic = (m_frequencyScale == LogFrequencyScale);
-
-    q0 = v->getFrequencyForY(y, minf, maxf, logarithmic);
-    q1 = v->getFrequencyForY(y - 1, minf, maxf, logarithmic);
-
-    // Now map these on to ("proportions of") actual bins, using raw
-    // FFT size (unsmoothed)
-
-    q0 = (q0 * getFFTSize(v)) / sr;
-    q1 = (q1 * getFFTSize(v)) / sr;
-
-    return true;
-}
-    
-bool
 SpectrogramLayer::getXBinRange(LayerGeometryProvider *v, int x, double &s0, double &s1) const
 {
     sv_frame_t modelStart = m_model->getStartFrame();
@@ -1303,8 +1184,8 @@
     sv_samplerate_t sr = m_model->getSampleRate();
 
     for (int q = q0i; q <= q1i; ++q) {
-	if (q == q0i) freqMin = (sr * q) / m_fftSize;
-	if (q == q1i) freqMax = (sr * (q+1)) / m_fftSize;
+	if (q == q0i) freqMin = (sr * q) / getFFTSize();
+	if (q == q1i) freqMax = (sr * (q+1)) / getFFTSize();
     }
     return true;
 }
@@ -1319,7 +1200,7 @@
 	return false;
     }
 
-    FFTModel *fft = getFFTModel(v);
+    FFTModel *fft = getFFTModel();
     if (!fft) return false;
 
     double s0 = 0, s1 = 0;
@@ -1338,22 +1219,23 @@
 
     bool haveAdj = false;
 
-    bool peaksOnly = (m_binDisplay == PeakBins ||
-		      m_binDisplay == PeakFrequencies);
+    bool peaksOnly = (m_binDisplay == BinDisplay::PeakBins ||
+		      m_binDisplay == BinDisplay::PeakFrequencies);
 
     for (int q = q0i; q <= q1i; ++q) {
 
 	for (int s = s0i; s <= s1i; ++s) {
 
-            if (!fft->isColumnAvailable(s)) continue;
-
 	    double binfreq = (double(sr) * q) / m_windowSize;
 	    if (q == q0i) freqMin = binfreq;
 	    if (q == q1i) freqMax = binfreq;
 
 	    if (peaksOnly && !fft->isLocalPeak(s, q)) continue;
 
-	    if (!fft->isOverThreshold(s, q, float(m_threshold * double(m_fftSize)/2.0))) continue;
+	    if (!fft->isOverThreshold
+                (s, q, float(m_threshold * double(getFFTSize())/2.0))) {
+                continue;
+            }
 
             double freq = binfreq;
 	    
@@ -1399,11 +1281,7 @@
 
     bool rv = false;
 
-    int zp = getZeroPadLevel(v);
-    q0i *= zp + 1;
-    q1i *= zp + 1;
-
-    FFTModel *fft = getFFTModel(v);
+    FFTModel *fft = getFFTModel();
 
     if (fft) {
 
@@ -1420,15 +1298,13 @@
             for (int s = s0i; s <= s1i; ++s) {
                 if (s >= 0 && q >= 0 && s < cw && q < ch) {
 
-                    if (!fft->isColumnAvailable(s)) continue;
-                    
                     double value;
 
                     value = fft->getPhaseAt(s, q);
                     if (!have || value < phaseMin) { phaseMin = value; }
                     if (!have || value > phaseMax) { phaseMax = value; }
 
-                    value = fft->getMagnitudeAt(s, q) / (m_fftSize/2.0);
+                    value = fft->getMagnitudeAt(s, q) / (getFFTSize()/2.0);
                     if (!have || value < min) { min = value; }
                     if (!have || value > max) { max = value; }
                     
@@ -1444,214 +1320,92 @@
 
     return rv;
 }
-   
-int
-SpectrogramLayer::getZeroPadLevel(const LayerGeometryProvider *v) const
-{
-    //!!! tidy all this stuff
-
-    if (m_binDisplay != AllBins) return 0;
-
-    Preferences::SpectrogramSmoothing smoothing = 
-        Preferences::getInstance()->getSpectrogramSmoothing();
-    
-    if (smoothing == Preferences::NoSpectrogramSmoothing ||
-        smoothing == Preferences::SpectrogramInterpolated) return 0;
-
-    if (m_frequencyScale == LogFrequencyScale) return 3;
-
-    sv_samplerate_t sr = m_model->getSampleRate();
-    
-    int maxbin = m_fftSize / 2;
-    if (m_maxFrequency > 0) {
-	maxbin = int((double(m_maxFrequency) * m_fftSize) / sr + 0.1);
-	if (maxbin > m_fftSize / 2) maxbin = m_fftSize / 2;
-    }
-
-    int minbin = 1;
-    if (m_minFrequency > 0) {
-	minbin = int((double(m_minFrequency) * m_fftSize) / sr + 0.1);
-	if (minbin < 1) minbin = 1;
-	if (minbin >= maxbin) minbin = maxbin - 1;
-    }
-
-    double perPixel =
-        double(v->getPaintHeight()) /
-        double((maxbin - minbin) / (m_zeroPadLevel + 1));
-
-    if (perPixel > 2.8) {
-        return 3; // 4x oversampling
-    } else if (perPixel > 1.5) {
-        return 1; // 2x
-    } else {
-        return 0; // 1x
-    }
-}
-
-int
-SpectrogramLayer::getFFTSize(const LayerGeometryProvider *v) const
-{
-    return m_fftSize * (getZeroPadLevel(v) + 1);
-}
 	
 FFTModel *
-SpectrogramLayer::getFFTModel(const LayerGeometryProvider *v) const
+SpectrogramLayer::getFFTModel() const
 {
     if (!m_model) return 0;
 
-    int fftSize = getFFTSize(v);
-
-    const View *view = v->getView();
+    int fftSize = getFFTSize();
+
+    //!!! it is now surely slower to do this on every getFFTModel()
+    //!!! request than it would be to recreate the model immediately
+    //!!! when something changes instead of just invalidating it
     
-    if (m_fftModels.find(view) != m_fftModels.end()) {
-        if (m_fftModels[view] == 0) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "SpectrogramLayer::getFFTModel(" << v << "): Found null model" << endl;
-#endif
-            return 0;
-        }
-        if (m_fftModels[view]->getHeight() != fftSize / 2 + 1) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "SpectrogramLayer::getFFTModel(" << v << "): Found a model with the wrong height (" << m_fftModels[view]->getHeight() << ", wanted " << (fftSize / 2 + 1) << ")" << endl;
-#endif
-            delete m_fftModels[view];
-            m_fftModels.erase(view);
-            delete m_peakCaches[view];
-            m_peakCaches.erase(view);
-        } else {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "SpectrogramLayer::getFFTModel(" << v << "): Found a good model of height " << m_fftModels[view]->getHeight() << endl;
-#endif
-            return m_fftModels[view];
-        }
+    if (m_fftModel &&
+        m_fftModel->getHeight() == fftSize / 2 + 1 &&
+        m_fftModel->getWindowIncrement() == getWindowIncrement()) {
+        return m_fftModel;
     }
-
-    if (m_fftModels.find(view) == m_fftModels.end()) {
-
-        FFTModel *model = new FFTModel(m_model,
-                                       m_channel,
-                                       m_windowType,
-                                       m_windowSize,
-                                       getWindowIncrement(),
-                                       fftSize);
-
-        if (!model->isOK()) {
-            QMessageBox::critical
-                (0, tr("FFT cache failed"),
-                 tr("Failed to create the FFT model for this spectrogram.\n"
-                    "There may be insufficient memory or disc space to continue."));
-            delete model;
-            m_fftModels[view] = 0;
-            return 0;
-        }
-
-        if (!m_sliceableModel) {
-#ifdef DEBUG_SPECTROGRAM
-            cerr << "SpectrogramLayer: emitting sliceableModelReplaced(0, " << model << ")" << endl;
-#endif
-            ((SpectrogramLayer *)this)->sliceableModelReplaced(0, model);
-            m_sliceableModel = model;
-        }
-
-        m_fftModels[view] = model;
+    
+    delete m_peakCache;
+    m_peakCache = 0;
+
+    delete m_fftModel;
+    m_fftModel = new FFTModel(m_model,
+                              m_channel,
+                              m_windowType,
+                              m_windowSize,
+                              getWindowIncrement(),
+                              fftSize);
+
+    if (!m_fftModel->isOK()) {
+        QMessageBox::critical
+            (0, tr("FFT cache failed"),
+             tr("Failed to create the FFT model for this spectrogram.\n"
+                "There may be insufficient memory or disc space to continue."));
+        delete m_fftModel;
+        m_fftModel = 0;
+        return 0;
     }
 
-    return m_fftModels[view];
+    ((SpectrogramLayer *)this)->sliceableModelReplaced(0, m_fftModel);
+
+    return m_fftModel;
 }
 
 Dense3DModelPeakCache *
-SpectrogramLayer::getPeakCache(const LayerGeometryProvider *v) const
+SpectrogramLayer::getPeakCache() const
 {
-    const View *view = v->getView();
-    if (!m_peakCaches[view]) {
-        FFTModel *f = getFFTModel(v);
+    //!!! see comment in getFFTModel
+    
+    if (!m_peakCache) {
+        FFTModel *f = getFFTModel();
         if (!f) return 0;
-        m_peakCaches[view] = new Dense3DModelPeakCache(f, 8);
+        m_peakCache = new Dense3DModelPeakCache(f, m_peakCacheDivisor);
     }
-    return m_peakCaches[view];
+    return m_peakCache;
 }
 
 const Model *
 SpectrogramLayer::getSliceableModel() const
 {
-    if (m_sliceableModel) return m_sliceableModel;
-    if (m_fftModels.empty()) return 0;
-    m_sliceableModel = m_fftModels.begin()->second;
-    return m_sliceableModel;
+    return m_fftModel;
 }
 
 void
-SpectrogramLayer::invalidateFFTModels()
+SpectrogramLayer::invalidateFFTModel()
 {
-    for (ViewFFTMap::iterator i = m_fftModels.begin();
-         i != m_fftModels.end(); ++i) {
-        delete i->second;
-    }
-    for (PeakCacheMap::iterator i = m_peakCaches.begin();
-         i != m_peakCaches.end(); ++i) {
-        delete i->second;
-    }
-    
-    m_fftModels.clear();
-    m_peakCaches.clear();
-
-    if (m_sliceableModel) {
-        cerr << "SpectrogramLayer: emitting sliceableModelReplaced(" << m_sliceableModel << ", 0)" << endl;
-        emit sliceableModelReplaced(m_sliceableModel, 0);
-        m_sliceableModel = 0;
-    }
+#ifdef DEBUG_SPECTROGRAM
+    cerr << "SpectrogramLayer::invalidateFFTModel called" << endl;
+#endif
+
+    emit sliceableModelReplaced(m_fftModel, 0);
+
+    delete m_fftModel;
+    delete m_peakCache;
+
+    m_fftModel = 0;
+    m_peakCache = 0;
 }
 
 void
 SpectrogramLayer::invalidateMagnitudes()
 {
+#ifdef DEBUG_SPECTROGRAM
+    cerr << "SpectrogramLayer::invalidateMagnitudes called" << endl;
+#endif
     m_viewMags.clear();
-    for (std::vector<MagnitudeRange>::iterator i = m_columnMags.begin();
-         i != m_columnMags.end(); ++i) {
-        *i = MagnitudeRange();
-    }
-}
-
-bool
-SpectrogramLayer::updateViewMagnitudes(LayerGeometryProvider *v) const
-{
-    MagnitudeRange mag;
-
-    int x0 = 0, x1 = v->getPaintWidth();
-    double s00 = 0, s01 = 0, s10 = 0, s11 = 0;
-    
-    if (!getXBinRange(v, x0, s00, s01)) {
-        s00 = s01 = double(m_model->getStartFrame()) / getWindowIncrement();
-    }
-
-    if (!getXBinRange(v, x1, s10, s11)) {
-        s10 = s11 = double(m_model->getEndFrame()) / getWindowIncrement();
-    }
-
-    int s0 = int(std::min(s00, s10) + 0.0001);
-    int s1 = int(std::max(s01, s11) + 0.0001);
-
-//    SVDEBUG << "SpectrogramLayer::updateViewMagnitudes: x0 = " << x0 << ", x1 = " << x1 << ", s00 = " << s00 << ", s11 = " << s11 << " s0 = " << s0 << ", s1 = " << s1 << endl;
-
-    if (int(m_columnMags.size()) <= s1) {
-        m_columnMags.resize(s1 + 1);
-    }
-
-    for (int s = s0; s <= s1; ++s) {
-        if (m_columnMags[s].isSet()) {
-            mag.sample(m_columnMags[s]);
-        }
-    }
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "SpectrogramLayer::updateViewMagnitudes returning from cols "
-              << s0 << " -> " << s1 << " inclusive" << endl;
-#endif
-
-    if (!mag.isSet()) return false;
-    if (mag == m_viewMags[v]) return false;
-    m_viewMags[v] = mag;
-    return true;
 }
 
 void
@@ -1660,22 +1414,149 @@
     m_synchronous = synchronous;
 }
 
+Colour3DPlotRenderer *
+SpectrogramLayer::getRenderer(LayerGeometryProvider *v) const
+{
+    int viewId = v->getId();
+    
+    if (m_renderers.find(viewId) == m_renderers.end()) {
+
+        Colour3DPlotRenderer::Sources sources;
+        sources.verticalBinLayer = this;
+        sources.fft = getFFTModel();
+        sources.source = sources.fft;
+        sources.peaks = getPeakCache();
+
+        ColourScale::Parameters cparams;
+        cparams.colourMap = m_colourMap;
+        cparams.scaleType = m_colourScale;
+        cparams.multiple = m_colourScaleMultiple;
+
+        if (m_colourScale != ColourScaleType::Phase) {
+            cparams.gain = m_gain;
+            cparams.threshold = m_threshold;
+        }
+
+        float minValue = 0.0f;
+        float maxValue = 1.0f;
+        
+        if (m_normalizeVisibleArea && m_viewMags[viewId].isSet()) {
+            minValue = m_viewMags[viewId].getMin();
+            maxValue = m_viewMags[viewId].getMax();
+        } else if (m_colourScale == ColourScaleType::Linear &&
+                   m_normalization == ColumnNormalization::None) {
+            maxValue = 0.1f;
+        }
+
+        if (maxValue <= minValue) {
+            maxValue = minValue + 0.1f;
+        }
+        if (maxValue <= m_threshold) {
+            maxValue = m_threshold + 0.1f;
+        }
+
+        cparams.minValue = minValue;
+        cparams.maxValue = maxValue;
+
+        m_lastRenderedMags[viewId] = MagnitudeRange(minValue, maxValue);
+
+        Colour3DPlotRenderer::Parameters params;
+        params.colourScale = ColourScale(cparams);
+        params.normalization = m_normalization;
+        params.binDisplay = m_binDisplay;
+        params.binScale = m_binScale;
+        params.alwaysOpaque = true;
+        params.invertVertical = false;
+        params.scaleFactor = 1.0;
+        params.colourRotation = m_colourRotation;
+
+        if (m_colourScale != ColourScaleType::Phase &&
+            m_normalization != ColumnNormalization::Hybrid) {
+            params.scaleFactor *= 2.f / float(getFFTSize());
+        }
+
+        Preferences::SpectrogramSmoothing smoothing = 
+            Preferences::getInstance()->getSpectrogramSmoothing();
+        params.interpolate = 
+            (smoothing == Preferences::SpectrogramInterpolated ||
+             smoothing == Preferences::SpectrogramZeroPaddedAndInterpolated);
+
+        m_renderers[v->getId()] = new Colour3DPlotRenderer(sources, params);
+    }
+
+    return m_renderers[v->getId()];
+}
+
+void
+SpectrogramLayer::paintWithRenderer(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
+{
+    Colour3DPlotRenderer *renderer = getRenderer(v);
+
+    Colour3DPlotRenderer::RenderResult result;
+    MagnitudeRange magRange;
+    int viewId = v->getId();
+
+    bool continuingPaint = !renderer->geometryChanged(v);
+    
+    if (continuingPaint) {
+        magRange = m_viewMags[viewId];
+    }
+    
+    if (m_synchronous) {
+
+        result = renderer->render(v, paint, rect);
+
+    } else {
+
+        result = renderer->renderTimeConstrained(v, paint, rect);
+
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+        cerr << "rect width from this paint: " << result.rendered.width()
+             << ", mag range in this paint: " << result.range.getMin() << " -> "
+             << result.range.getMax() << endl;
+#endif
+        
+        QRect uncached = renderer->getLargestUncachedRect(v);
+        if (uncached.width() > 0) {
+            v->updatePaintRect(uncached);
+        }
+    }
+
+    magRange.sample(result.range);
+
+    if (magRange.isSet()) {
+        if (m_viewMags[viewId] != magRange) {
+            m_viewMags[viewId] = magRange;
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+            cerr << "mag range in this view has changed: "
+                 << magRange.getMin() << " -> " << magRange.getMax() << endl;
+#endif
+        }
+    }
+
+    if (!continuingPaint && m_normalizeVisibleArea &&
+        m_viewMags[viewId] != m_lastRenderedMags[viewId]) {
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+        cerr << "mag range has changed from last rendered range: re-rendering"
+             << endl;
+#endif
+        delete m_renderers[viewId];
+        m_renderers.erase(viewId);
+        v->updatePaintRect(v->getPaintRect());
+    }
+}
+
 void
 SpectrogramLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
 {
-    // What a lovely, old-fashioned function this is.
-    // It's practically FORTRAN 77 in its clarity and linearity.
-
     Profiler profiler("SpectrogramLayer::paint", false);
 
 #ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "SpectrogramLayer::paint(): m_model is " << m_model << ", zoom level is " << v->getZoomLevel() << endl;
+    cerr << "SpectrogramLayer::paint() entering: m_model is " << m_model << ", zoom level is " << v->getZoomLevel() << endl;
     
-    cerr << "rect is " << rect.x() << "," << rect.y() << " " << rect.width() << "x" << rect.height() << endl;
+    cerr << "SpectrogramLayer::paint(): rect is " << rect.x() << "," << rect.y() << " " << rect.width() << "x" << rect.height() << endl;
 #endif
 
-    sv_frame_t startFrame = v->getStartFrame();
-
     if (!m_model || !m_model->isOK() || !m_model->isReady()) {
 	return;
     }
@@ -1684,1025 +1565,9 @@
 	SVDEBUG << "SpectrogramLayer::paint(): Layer is dormant, making it undormant again" << endl;
     }
 
-    // Need to do this even if !isLayerDormant, as that could mean v
-    // is not in the dormancy map at all -- we need it to be present
-    // and accountable for when determining whether we need the cache
-    // in the cache-fill thread above.
-    //!!! no inter use cache-fill thread
-    const_cast<SpectrogramLayer *>(this)->Layer::setLayerDormant(v, false);
-
-    int fftSize = getFFTSize(v);
-/*
-    FFTModel *fft = getFFTModel(v);
-    if (!fft) {
-	cerr << "ERROR: SpectrogramLayer::paint(): No FFT model, returning" << endl;
-	return;
-    }
-*/
-
-    const View *view = v->getView();
-    
-    ImageCache &cache = m_imageCaches[view];
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "SpectrogramLayer::paint(): image cache valid area " << cache.
-
-validArea.x() << ", " << cache.validArea.y() << ", " << cache.validArea.width() << "x" << cache.validArea.height() << endl;
-#endif
-
-    int zoomLevel = v->getZoomLevel();
-
-    int x0 = 0;
-    int x1 = v->getPaintWidth();
-
-    bool recreateWholeImageCache = true;
-
-    x0 = rect.left();
-    x1 = rect.right() + 1;
-/*
-    double xPixelRatio = double(fft->getResolution()) / double(zoomLevel);
-    cerr << "xPixelRatio = " << xPixelRatio << endl;
-    if (xPixelRatio < 1.f) xPixelRatio = 1.f;
-*/
-    if (cache.validArea.width() > 0) {
-
-        int cw = cache.image.width();
-        int ch = cache.image.height();
-        
-	if (int(cache.zoomLevel) == zoomLevel &&
-	    cw == v->getPaintWidth() &&
-	    ch == v->getPaintHeight()) {
-
-	    if (v->getXForFrame(cache.startFrame) ==
-		v->getXForFrame(startFrame) &&
-                cache.validArea.x() <= x0 &&
-                cache.validArea.x() + cache.validArea.width() >= x1) {
-	    
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-		cerr << "SpectrogramLayer: image cache good" << endl;
-#endif
-
-		paint.drawImage(rect, cache.image, rect);
-                //!!!
-//                paint.drawImage(v->rect(), cache.image,
-//                                QRect(QPoint(0, 0), cache.image.size()));
-
-                illuminateLocalFeatures(v, paint);
-		return;
-
-	    } else {
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-		cerr << "SpectrogramLayer: image cache partially OK" << endl;
-#endif
-
-		recreateWholeImageCache = false;
-
-		int dx = v->getXForFrame(cache.startFrame) -
-		         v->getXForFrame(startFrame);
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-		cerr << "SpectrogramLayer: dx = " << dx << " (image cache " << cw << "x" << ch << ")" << endl;
-#endif
-
-		if (dx != 0 &&
-                    dx > -cw &&
-                    dx <  cw) {
-                    
-                    int dxp = dx;
-                    if (dxp < 0) dxp = -dxp;
-                    size_t copy = (cw - dxp) * sizeof(QRgb);
-                    for (int y = 0; y < ch; ++y) {
-                        QRgb *line = (QRgb *)cache.image.scanLine(y);
-                        if (dx < 0) {
-                            memmove(line, line + dxp, copy);
-                        } else {
-                            memmove(line + dxp, line, copy);
-                        }
-                    }
-
-                    int px = cache.validArea.x();
-                    int pw = cache.validArea.width();
-
-		    if (dx < 0) {
-			x0 = cw + dx;
-			x1 = cw;
-                        px += dx;
-                        if (px < 0) {
-                            pw += px;
-                            px = 0;
-                            if (pw < 0) pw = 0;
-                        }
-		    } else {
-			x0 = 0;
-			x1 = dx;
-                        px += dx;
-                        if (px + pw > cw) {
-                            pw = int(cw) - px;
-                            if (pw < 0) pw = 0;
-                        }
-		    }
-                    
-                    cache.validArea =
-                        QRect(px, cache.validArea.y(),
-                              pw, cache.validArea.height());
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    cerr << "valid area now "
-                              << px << "," << cache.validArea.y()
-                              << " " << pw << "x" << cache.validArea.height()
-                              << endl;
-#endif
-/*
-		    paint.drawImage(rect & cache.validArea,
-                                     cache.image,
-                                     rect & cache.validArea);
-*/
-                } else if (dx != 0) {
-
-                    // we scrolled too far to be of use
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    cerr << "dx == " << dx << ": scrolled too far for cache to be useful" << endl;
-#endif
-
-                    cache.validArea = QRect();
-                    recreateWholeImageCache = true;
-                }
-	    }
-	} else {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-	    cerr << "SpectrogramLayer: image cache useless" << endl;
-            if (int(cache.zoomLevel) != zoomLevel) {
-                cerr << "(cache zoomLevel " << cache.zoomLevel
-                          << " != " << zoomLevel << ")" << endl;
-            }
-            if (cw != v->getPaintWidth()) {
-                cerr << "(cache width " << cw
-                          << " != " << v->getPaintWidth();
-            }
-            if (ch != v->getPaintHeight()) {
-                cerr << "(cache height " << ch
-                          << " != " << v->getPaintHeight();
-            }
-#endif
-            cache.validArea = QRect();
-//            recreateWholeImageCache = true;
-	}
-    }
-
-    if (updateViewMagnitudes(v)) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "SpectrogramLayer: magnitude range changed to [" << m_viewMags[v].getMin() << "->" << m_viewMags[v].getMax() << "]" << endl;
-#endif
-        if (m_normalization == NormalizeVisibleArea) {
-            cache.validArea = QRect();
-            recreateWholeImageCache = true;
-        }
-    } else {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "No change in magnitude range [" << m_viewMags[v].getMin() << "->" << m_viewMags[v].getMax() << "]" << endl;
-#endif
-    }
-
-    if (recreateWholeImageCache) {
-        x0 = 0;
-        x1 = v->getPaintWidth();
-    }
-
-    struct timeval tv;
-    (void)gettimeofday(&tv, 0);
-    RealTime mainPaintStart = RealTime::fromTimeval(tv);
-
-    int paintBlockWidth = m_lastPaintBlockWidth;
-
-    if (m_synchronous) {
-        if (paintBlockWidth < x1 - x0) {
-            // always paint full width
-            paintBlockWidth = x1 - x0;
-        }
-    } else {
-        if (paintBlockWidth == 0) {
-            paintBlockWidth = (300000 / zoomLevel);
-        } else {
-            RealTime lastTime = m_lastPaintTime;
-            while (lastTime > RealTime::fromMilliseconds(200) &&
-                   paintBlockWidth > 100) {
-                paintBlockWidth /= 2;
-                lastTime = lastTime / 2;
-            }
-            while (lastTime < RealTime::fromMilliseconds(90) &&
-                   paintBlockWidth < 1500) {
-                paintBlockWidth *= 2;
-                lastTime = lastTime * 2;
-            }
-        }
-        
-        if (paintBlockWidth < 50) paintBlockWidth = 50;
-    }
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "[" << this << "]: last paint width: " << m_lastPaintBlockWidth << ", last paint time: " << m_lastPaintTime << ", new paint width: " << paintBlockWidth << endl;
-#endif
-
-    // We always paint the full height when refreshing the cache.
-    // Smaller heights can be used when painting direct from cache
-    // (further up in this function), but we want to ensure the cache
-    // is coherent without having to worry about vertical matching of
-    // required and valid areas as well as horizontal.
-
-    int h = v->getPaintHeight();
-
-    if (cache.validArea.width() > 0) {
-
-        // If part of the cache is known to be valid, select a strip
-        // immediately to left or right of the valid part
-
-        //!!! this really needs to be coordinated with the selection
-        //!!! of m_drawBuffer boundaries in the bufferBinResolution
-        //!!! case below
-
-        int vx0 = 0, vx1 = 0;
-        vx0 = cache.validArea.x();
-        vx1 = cache.validArea.x() + cache.validArea.width();
-        
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "x0 " << x0 << ", x1 " << x1 << ", vx0 " << vx0 << ", vx1 " << vx1 << ", paintBlockWidth " << paintBlockWidth << endl;
-#endif         
-        if (x0 < vx0) {
-            if (x0 + paintBlockWidth < vx0) {
-                x0 = vx0 - paintBlockWidth;
-            }
-            x1 = vx0;
-        } else if (x0 >= vx1) {
-            x0 = vx1;
-            if (x1 > x0 + paintBlockWidth) {
-                x1 = x0 + paintBlockWidth;
-            }
-        } else {
-            // x0 is within the valid area
-            if (x1 > vx1) {
-                x0 = vx1;
-                if (x0 + paintBlockWidth < x1) {
-                    x1 = x0 + paintBlockWidth;
-                }
-            } else {
-                x1 = x0; // it's all valid, paint nothing
-            }
-        }
-         
-        cache.validArea = QRect
-            (std::min(vx0, x0), cache.validArea.y(),
-             std::max(vx1 - std::min(vx0, x0),
-                       x1 - std::min(vx0, x0)),
-             cache.validArea.height());
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "Valid area becomes " << cache.validArea.x()
-                  << ", " << cache.validArea.y() << ", "
-                  << cache.validArea.width() << "x"
-                  << cache.validArea.height() << endl;
-#endif
-            
-    } else {
-        if (x1 > x0 + paintBlockWidth) {
-            int sfx = x1;
-            if (startFrame < 0) sfx = v->getXForFrame(0);
-            if (sfx >= x0 && sfx + paintBlockWidth <= x1) {
-                x0 = sfx;
-                x1 = x0 + paintBlockWidth;
-            } else {
-                int mid = (x1 + x0) / 2;
-                x0 = mid - paintBlockWidth/2;
-                x1 = x0 + paintBlockWidth;
-            }
-        }
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "Valid area becomes " << x0 << ", 0, " << (x1-x0)
-                  << "x" << h << endl;
-#endif
-        cache.validArea = QRect(x0, 0, x1 - x0, h);
-    }
-
-/*
-    if (xPixelRatio != 1.f) {
-        x0 = int((int(x0 / xPixelRatio) - 4) * xPixelRatio + 0.0001);
-        x1 = int((int(x1 / xPixelRatio) + 4) * xPixelRatio + 0.0001);
-    }
-*/
-    int w = x1 - x0;
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "x0 " << x0 << ", x1 " << x1 << ", w " << w << ", h " << h << endl;
-#endif
-
-    sv_samplerate_t sr = m_model->getSampleRate();
-
-    // Set minFreq and maxFreq to the frequency extents of the possibly
-    // zero-padded visible bin range, and displayMinFreq and displayMaxFreq
-    // to the actual scale frequency extents (presumably not zero padded).
-
-    // If we are zero padding, we want to use the zero-padded
-    // equivalents of the bins that we would be using if not zero
-    // padded, to avoid spaces at the top and bottom of the display.
-
-    // Note fftSize is the actual zero-padded fft size, m_fftSize the
-    // nominal fft size.
-    
-    int maxbin = m_fftSize / 2;
-    if (m_maxFrequency > 0) {
-	maxbin = int((double(m_maxFrequency) * m_fftSize) / sr + 0.001);
-	if (maxbin > m_fftSize / 2) maxbin = m_fftSize / 2;
-    }
-
-    int minbin = 1;
-    if (m_minFrequency > 0) {
-	minbin = int((double(m_minFrequency) * m_fftSize) / sr + 0.001);
-//        cerr << "m_minFrequency = " << m_minFrequency << " -> minbin = " << minbin << endl;
-	if (minbin < 1) minbin = 1;
-	if (minbin >= maxbin) minbin = maxbin - 1;
-    }
-
-    int zpl = getZeroPadLevel(v) + 1;
-    minbin = minbin * zpl;
-    maxbin = (maxbin + 1) * zpl - 1;
-
-    double minFreq = (double(minbin) * sr) / fftSize;
-    double maxFreq = (double(maxbin) * sr) / fftSize;
-
-    double displayMinFreq = minFreq;
-    double displayMaxFreq = maxFreq;
-
-    if (fftSize != m_fftSize) {
-        displayMinFreq = getEffectiveMinFrequency();
-        displayMaxFreq = getEffectiveMaxFrequency();
-    }
-
-//    cerr << "(giving actual minFreq " << minFreq << " and display minFreq " << displayMinFreq << ")" << endl;
-
-    int increment = getWindowIncrement();
-    
-    bool logarithmic = (m_frequencyScale == LogFrequencyScale);
-/*
-    double yforbin[maxbin - minbin + 1];
-
-    for (int q = minbin; q <= maxbin; ++q) {
-        double f0 = (double(q) * sr) / fftSize;
-        yforbin[q - minbin] =
-            v->getYForFrequency(f0, displayMinFreq, displayMaxFreq,
-                                logarithmic);
-    }
-*/
-    MagnitudeRange overallMag = m_viewMags[v];
-    bool overallMagChanged = false;
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << ((double(v->getFrameForX(1) - v->getFrameForX(0))) / increment) << " bin(s) per pixel" << endl;
-#endif
-
-    if (w == 0) {
-        SVDEBUG << "*** NOTE: w == 0" << endl;
-    }
-
-    Profiler outerprof("SpectrogramLayer::paint: all cols");
-
-    // The draw buffer contains a fragment at either our pixel
-    // resolution (if there is more than one time-bin per pixel) or
-    // time-bin resolution (if a time-bin spans more than one pixel).
-    // We need to ensure that it starts and ends at points where a
-    // time-bin boundary occurs at an exact pixel boundary, and with a
-    // certain amount of overlap across existing pixels so that we can
-    // scale and draw from it without smoothing errors at the edges.
-
-    // If (getFrameForX(x) / increment) * increment ==
-    // getFrameForX(x), then x is a time-bin boundary.  We want two
-    // such boundaries at either side of the draw buffer -- one which
-    // we draw up to, and one which we subsequently crop at.
-
-    bool bufferBinResolution = false;
-    if (increment > zoomLevel) bufferBinResolution = true;
-
-    sv_frame_t leftBoundaryFrame = -1, leftCropFrame = -1;
-    sv_frame_t rightBoundaryFrame = -1, rightCropFrame = -1;
-
-    int bufwid;
-
-    if (bufferBinResolution) {
-
-        for (int x = x0; ; --x) {
-            sv_frame_t f = v->getFrameForX(x);
-            if ((f / increment) * increment == f) {
-                if (leftCropFrame == -1) leftCropFrame = f;
-                else if (x < x0 - 2) { leftBoundaryFrame = f; break; }
-            }
-        }
-        for (int x = x0 + w; ; ++x) {
-            sv_frame_t f = v->getFrameForX(x);
-            if ((f / increment) * increment == f) {
-                if (rightCropFrame == -1) rightCropFrame = f;
-                else if (x > x0 + w + 2) { rightBoundaryFrame = f; break; }
-            }
-        }
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "Left: crop: " << leftCropFrame << " (bin " << leftCropFrame/increment << "); boundary: " << leftBoundaryFrame << " (bin " << leftBoundaryFrame/increment << ")" << endl;
-        cerr << "Right: crop: " << rightCropFrame << " (bin " << rightCropFrame/increment << "); boundary: " << rightBoundaryFrame << " (bin " << rightBoundaryFrame/increment << ")" << endl;
-#endif
-
-        bufwid = int((rightBoundaryFrame - leftBoundaryFrame) / increment);
-
-    } else {
-        
-        bufwid = w;
-    }
-
-    vector<int> binforx(bufwid);
-    vector<double> binfory(h);
-    
-    bool usePeaksCache = false;
-
-    if (bufferBinResolution) {
-        for (int x = 0; x < bufwid; ++x) {
-            binforx[x] = int(leftBoundaryFrame / increment) + x;
-//            cerr << "binforx[" << x << "] = " << binforx[x] << endl;
-        }
-        m_drawBuffer = QImage(bufwid, h, QImage::Format_Indexed8);
-    } else {
-        for (int x = 0; x < bufwid; ++x) {
-            double s0 = 0, s1 = 0;
-            if (getXBinRange(v, x + x0, s0, s1)) {
-                binforx[x] = int(s0 + 0.0001);
-            } else {
-                binforx[x] = -1; //???
-            }
-        }
-        if (m_drawBuffer.width() < bufwid || m_drawBuffer.height() < h) {
-            m_drawBuffer = QImage(bufwid, h, QImage::Format_Indexed8);
-        }
-        usePeaksCache = (increment * 8) < zoomLevel;
-        if (m_colourScale == PhaseColourScale) usePeaksCache = false;
-    }
-
-// No longer exists in Qt5:    m_drawBuffer.setNumColors(256);
-    for (int pixel = 0; pixel < 256; ++pixel) {
-        m_drawBuffer.setColor((unsigned char)pixel,
-                              m_palette.getColour((unsigned char)pixel).rgb());
-    }
-
-    m_drawBuffer.fill(0);
-    
-    if (m_binDisplay != PeakFrequencies) {
-
-        for (int y = 0; y < h; ++y) {
-            double q0 = 0, q1 = 0;
-            if (!getSmoothedYBinRange(v, h-y-1, q0, q1)) {
-                binfory[y] = -1;
-            } else {
-                binfory[y] = q0;
-//                cerr << "binfory[" << y << "] = " << binfory[y] << endl;
-            }
-        }
-
-        paintDrawBuffer(v, bufwid, h, binforx, binfory, usePeaksCache,
-                        overallMag, overallMagChanged);
-
-    } else {
-
-        paintDrawBufferPeakFrequencies(v, bufwid, h, binforx,
-                                       minbin, maxbin,
-                                       displayMinFreq, displayMaxFreq,
-                                       logarithmic,
-                                       overallMag, overallMagChanged);
-    }
-
-/*
-    for (int x = 0; x < w / xPixelRatio; ++x) {
-
-        Profiler innerprof("SpectrogramLayer::paint: 1 pixel column");
-
-        runOutOfData = !paintColumnValues(v, fft, x0, x,
-                                          minbin, maxbin,
-                                          displayMinFreq, displayMaxFreq,
-                                          xPixelRatio,
-                                          h, yforbin);
-
-        if (runOutOfData) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "Run out of data -- dropping out of loop" << endl;
-#endif
-            break;
-        }
-    }
-*/
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-//    cerr << pixels << " pixels drawn" << endl;
-#endif
-
-    if (overallMagChanged) {
-        m_viewMags[v] = overallMag;
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "Overall mag is now [" << m_viewMags[v].getMin() << "->" << m_viewMags[v].getMax() << "] - will be updating" << endl;
-#endif
-    } else {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "Overall mag unchanged at [" << m_viewMags[v].getMin() << "->" << m_viewMags[v].getMax() << "]" << endl;
-#endif
-    }
-
-    outerprof.end();
-
-    Profiler profiler2("SpectrogramLayer::paint: draw image");
-
-    if (recreateWholeImageCache) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "Recreating image cache: width = " << v->getPaintWidth()
-                  << ", height = " << h << endl;
-#endif
-	cache.image = QImage(v->getPaintWidth(), h, QImage::Format_ARGB32_Premultiplied);
-    }
-
-    if (w > 0) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-        cerr << "Painting " << w << "x" << h
-                  << " from draw buffer at " << 0 << "," << 0
-                  << " to " << w << "x" << h << " on cache at "
-                  << x0 << "," << 0 << endl;
-#endif
-
-        QPainter cachePainter(&cache.image);
-
-        if (bufferBinResolution) {
-            int scaledLeft = v->getXForFrame(leftBoundaryFrame);
-            int scaledRight = v->getXForFrame(rightBoundaryFrame);
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "Rescaling image from " << bufwid
-                 << "x" << h << " to "
-                 << scaledRight-scaledLeft << "x" << h << endl;
-#endif
-            Preferences::SpectrogramXSmoothing xsmoothing = 
-                Preferences::getInstance()->getSpectrogramXSmoothing();
-//            SVDEBUG << "xsmoothing == " << xsmoothing << endl;
-            QImage scaled = m_drawBuffer.scaled
-                (scaledRight - scaledLeft, h,
-                 Qt::IgnoreAspectRatio,
-                 ((xsmoothing == Preferences::SpectrogramXInterpolated) ?
-                  Qt::SmoothTransformation : Qt::FastTransformation));
-            int scaledLeftCrop = v->getXForFrame(leftCropFrame);
-            int scaledRightCrop = v->getXForFrame(rightCropFrame);
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-            cerr << "Drawing image region of width " << scaledRightCrop - scaledLeftCrop << " to "
-                 << scaledLeftCrop << " from " << scaledLeftCrop - scaledLeft << endl;
-#endif
-            cachePainter.drawImage
-                (QRect(scaledLeftCrop, 0,
-                       scaledRightCrop - scaledLeftCrop, h),
-                 scaled,
-                 QRect(scaledLeftCrop - scaledLeft, 0,
-                       scaledRightCrop - scaledLeftCrop, h));
-        } else {
-            cachePainter.drawImage(QRect(x0, 0, w, h),
-                                   m_drawBuffer,
-                                   QRect(0, 0, w, h));
-        }
-
-        cachePainter.end();
-    }
-
-    QRect pr = rect & cache.validArea;
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "Painting " << pr.width() << "x" << pr.height()
-              << " from cache at " << pr.x() << "," << pr.y()
-              << " to window" << endl;
-#endif
-
-    paint.drawImage(pr.x(), pr.y(), cache.image,
-                    pr.x(), pr.y(), pr.width(), pr.height());
-    //!!!
-//    paint.drawImage(v->rect(), cache.image,
-//                    QRect(QPoint(0, 0), cache.image.size()));
-
-    cache.startFrame = startFrame;
-    cache.zoomLevel = zoomLevel;
-
-    if (!m_synchronous) {
-
-        if ((m_normalization != NormalizeVisibleArea) || !overallMagChanged) {
-    
-            if (cache.validArea.x() > 0) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                cerr << "SpectrogramLayer::paint() updating left (0, "
-                          << cache.validArea.x() << ")" << endl;
-#endif
-                v->getView()->update(0, 0, cache.validArea.x(), h);
-            }
-            
-            if (cache.validArea.x() + cache.validArea.width() <
-                cache.image.width()) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                cerr << "SpectrogramLayer::paint() updating right ("
-                          << cache.validArea.x() + cache.validArea.width()
-                          << ", "
-                          << cache.image.width() - (cache.validArea.x() +
-                                                     cache.validArea.width())
-                          << ")" << endl;
-#endif
-                v->getView()->update(cache.validArea.x() + cache.validArea.width(),
-                          0,
-                          cache.image.width() - (cache.validArea.x() +
-                                                  cache.validArea.width()),
-                          h);
-            }
-        } else {
-            // overallMagChanged
-            cerr << "\noverallMagChanged - updating all\n" << endl;
-            cache.validArea = QRect();
-            v->getView()->update();
-        }
-    }
+    paintWithRenderer(v, paint, rect);
 
     illuminateLocalFeatures(v, paint);
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "SpectrogramLayer::paint() returning" << endl;
-#endif
-
-    if (!m_synchronous) {
-        m_lastPaintBlockWidth = paintBlockWidth;
-        (void)gettimeofday(&tv, 0);
-        m_lastPaintTime = RealTime::fromTimeval(tv) - mainPaintStart;
-    }
-}
-
-bool
-SpectrogramLayer::paintDrawBufferPeakFrequencies(LayerGeometryProvider *v,
-                                                 int w,
-                                                 int h,
-                                                 const vector<int> &binforx,
-                                                 int minbin,
-                                                 int maxbin,
-                                                 double displayMinFreq,
-                                                 double displayMaxFreq,
-                                                 bool logarithmic,
-                                                 MagnitudeRange &overallMag,
-                                                 bool &overallMagChanged) const
-{
-    Profiler profiler("SpectrogramLayer::paintDrawBufferPeakFrequencies");
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "minbin " << minbin << ", maxbin " << maxbin << "; w " << w << ", h " << h << endl;
-#endif
-    if (minbin < 0) minbin = 0;
-    if (maxbin < 0) maxbin = minbin+1;
-
-    FFTModel *fft = getFFTModel(v);
-    if (!fft) return false;
-
-    FFTModel::PeakSet peakfreqs;
-
-    int psx = -1;
-
-#ifdef __GNUC__
-    float values[maxbin - minbin + 1];
-#else
-    float *values = (float *)alloca((maxbin - minbin + 1) * sizeof(float));
-#endif
-
-    for (int x = 0; x < w; ++x) {
-        
-        if (binforx[x] < 0) continue;
-
-        int sx0 = binforx[x];
-        int sx1 = sx0;
-        if (x+1 < w) sx1 = binforx[x+1];
-        if (sx0 < 0) sx0 = sx1 - 1;
-        if (sx0 < 0) continue;
-        if (sx1 <= sx0) sx1 = sx0 + 1;
-
-        for (int sx = sx0; sx < sx1; ++sx) {
-
-            if (sx < 0 || sx >= int(fft->getWidth())) continue;
-
-            if (!m_synchronous) {
-                if (!fft->isColumnAvailable(sx)) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    cerr << "Met unavailable column at col " << sx << endl;
-#endif
-                    return false;
-                }
-            }
-
-            MagnitudeRange mag;
-
-            if (sx != psx) {
-                peakfreqs = fft->getPeakFrequencies(FFTModel::AllPeaks, sx,
-                                                    minbin, maxbin - 1);
-                if (m_colourScale == PhaseColourScale) {
-                    fft->getPhasesAt(sx, values, minbin, maxbin - minbin + 1);
-                } else if (m_normalization == NormalizeColumns) {
-                    fft->getNormalizedMagnitudesAt(sx, values, minbin, maxbin - minbin + 1);
-                } else if (m_normalization == NormalizeHybrid) {
-                    float max = fft->getNormalizedMagnitudesAt(sx, values, minbin, maxbin - minbin + 1);
-                    if (max > 0.f) {
-                        for (int i = minbin; i <= maxbin; ++i) {
-                            values[i - minbin] = float(values[i - minbin] *
-                                                       log10f(max));
-                        }
-                    }
-                } else {
-                    fft->getMagnitudesAt(sx, values, minbin, maxbin - minbin + 1);
-                }
-                psx = sx;
-            }
-
-            for (FFTModel::PeakSet::const_iterator pi = peakfreqs.begin();
-                 pi != peakfreqs.end(); ++pi) {
-
-                int bin = pi->first;
-                double freq = pi->second;
-
-                if (bin < minbin) continue;
-                if (bin > maxbin) break;
-
-                double value = values[bin - minbin];
-
-                if (m_colourScale != PhaseColourScale) {
-                    if (m_normalization != NormalizeColumns) {
-                        value /= (m_fftSize/2.0);
-                    }
-                    mag.sample(float(value));
-                    value *= m_gain;
-                }
-
-                double y = v->getYForFrequency
-                    (freq, displayMinFreq, displayMaxFreq, logarithmic);
-
-                int iy = int(y + 0.5);
-                if (iy < 0 || iy >= h) continue;
-
-                m_drawBuffer.setPixel(x, iy, getDisplayValue(v, value));
-            }
-
-            if (mag.isSet()) {
-                if (sx >= int(m_columnMags.size())) {
-#ifdef DEBUG_SPECTROGRAM
-                    cerr << "INTERNAL ERROR: " << sx << " >= "
-                              << m_columnMags.size()
-                              << " at SpectrogramLayer.cpp::paintDrawBuffer"
-                              << endl;
-#endif
-                } else {
-                    m_columnMags[sx].sample(mag);
-                    if (overallMag.sample(mag)) overallMagChanged = true;
-                }
-            }
-        }
-    }
-
-    return true;
-}
-
-bool
-SpectrogramLayer::paintDrawBuffer(LayerGeometryProvider *v,
-                                  int w,
-                                  int h,
-                                  const vector<int> &binforx,
-                                  const vector<double> &binfory,
-                                  bool usePeaksCache,
-                                  MagnitudeRange &overallMag,
-                                  bool &overallMagChanged) const
-{
-    Profiler profiler("SpectrogramLayer::paintDrawBuffer");
-
-    int minbin = int(binfory[0] + 0.0001);
-    int maxbin = int(binfory[h-1]);
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "minbin " << minbin << ", maxbin " << maxbin << "; w " << w << ", h " << h << endl;
-#endif
-    if (minbin < 0) minbin = 0;
-    if (maxbin < 0) maxbin = minbin+1;
-
-    DenseThreeDimensionalModel *sourceModel = 0;
-    FFTModel *fft = 0;
-    int divisor = 1;
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-    cerr << "Note: bin display = " << m_binDisplay << ", w = " << w << ", binforx[" << w-1 << "] = " << binforx[w-1] << ", binforx[0] = " << binforx[0] << endl;
-#endif
-    if (usePeaksCache) { //!!!
-        sourceModel = getPeakCache(v);
-        divisor = 8;//!!!
-        minbin = 0;
-        maxbin = sourceModel->getHeight();
-    } else {
-        sourceModel = fft = getFFTModel(v);
-    }
-
-    if (!sourceModel) return false;
-
-    bool interpolate = false;
-    Preferences::SpectrogramSmoothing smoothing = 
-        Preferences::getInstance()->getSpectrogramSmoothing();
-    if (smoothing == Preferences::SpectrogramInterpolated ||
-        smoothing == Preferences::SpectrogramZeroPaddedAndInterpolated) {
-        if (m_binDisplay != PeakBins &&
-            m_binDisplay != PeakFrequencies) {
-            interpolate = true;
-        }
-    }
-
-    int psx = -1;
-
-#ifdef __GNUC__
-    float autoarray[maxbin - minbin + 1];
-    float peaks[h];
-#else
-    float *autoarray = (float *)alloca((maxbin - minbin + 1) * sizeof(float));
-    float *peaks = (float *)alloca(h * sizeof(float));
-#endif
-
-    const float *values = autoarray;
-    DenseThreeDimensionalModel::Column c;
-
-    for (int x = 0; x < w; ++x) {
-        
-        if (binforx[x] < 0) continue;
-
-//        float columnGain = m_gain;
-        float columnMax = 0.f;
-
-        int sx0 = binforx[x] / divisor;
-        int sx1 = sx0;
-        if (x+1 < w) sx1 = binforx[x+1] / divisor;
-        if (sx0 < 0) sx0 = sx1 - 1;
-        if (sx0 < 0) continue;
-        if (sx1 <= sx0) sx1 = sx0 + 1;
-
-        for (int y = 0; y < h; ++y) peaks[y] = 0.f;
-            
-        for (int sx = sx0; sx < sx1; ++sx) {
-
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-//            cerr << "sx = " << sx << endl;
-#endif
-
-            if (sx < 0 || sx >= int(sourceModel->getWidth())) continue;
-
-            if (!m_synchronous) {
-                if (!sourceModel->isColumnAvailable(sx)) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    cerr << "Met unavailable column at col " << sx << endl;
-#endif
-                    return false;
-                }
-            }
-
-            MagnitudeRange mag;
-
-            if (sx != psx) {
-                if (fft) {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    cerr << "Retrieving column " << sx << " from fft directly" << endl;
-#endif
-                    if (m_colourScale == PhaseColourScale) {
-                        fft->getPhasesAt(sx, autoarray, minbin, maxbin - minbin + 1);
-                    } else if (m_normalization == NormalizeColumns) {
-                        fft->getNormalizedMagnitudesAt(sx, autoarray, minbin, maxbin - minbin + 1);
-                    } else if (m_normalization == NormalizeHybrid) {
-                        float max = fft->getNormalizedMagnitudesAt(sx, autoarray, minbin, maxbin - minbin + 1);
-                        float scale = log10f(max + 1.f);
-//                        cout << "sx = " << sx << ", max = " << max << ", log10(max) = " << log10(max) << ", scale = " << scale << endl;
-                        for (int i = minbin; i <= maxbin; ++i) {
-                            autoarray[i - minbin] *= scale;
-                        }
-                    } else {
-                        fft->getMagnitudesAt(sx, autoarray, minbin, maxbin - minbin + 1);
-                    }
-                } else {
-#ifdef DEBUG_SPECTROGRAM_REPAINT
-                    cerr << "Retrieving column " << sx << " from peaks cache" << endl;
-#endif
-                    c = sourceModel->getColumn(sx);
-                    if (m_normalization == NormalizeColumns ||
-                        m_normalization == NormalizeHybrid) {
-                        for (int y = 0; y < h; ++y) {
-                            if (c[y] > columnMax) columnMax = c[y];
-                        }
-                    }
-                    values = c.constData() + minbin;
-                }
-                psx = sx;
-            }
-
-            for (int y = 0; y < h; ++y) {
-
-                double sy0 = binfory[y];
-                double sy1 = sy0 + 1;
-                if (y+1 < h) sy1 = binfory[y+1];
-
-                double value = 0.0;
-
-                if (interpolate && fabs(sy1 - sy0) < 1.0) {
-
-                    double centre = (sy0 + sy1) / 2;
-                    double dist = (centre - 0.5) - rint(centre - 0.5);
-                    int bin = int(centre);
-                    int other = (dist < 0 ? (bin-1) : (bin+1));
-                    if (bin < minbin) bin = minbin;
-                    if (bin > maxbin) bin = maxbin;
-                    if (other < minbin || other > maxbin) other = bin;
-                    double prop = 1.0 - fabs(dist);
-
-                    double v0 = values[bin - minbin];
-                    double v1 = values[other - minbin];
-                    if (m_binDisplay == PeakBins) {
-                        if (bin == minbin || bin == maxbin ||
-                            v0 < values[bin-minbin-1] ||
-                            v0 < values[bin-minbin+1]) v0 = 0.0;
-                        if (other == minbin || other == maxbin ||
-                            v1 < values[other-minbin-1] ||
-                            v1 < values[other-minbin+1]) v1 = 0.0;
-                    }
-                    if (v0 == 0.0 && v1 == 0.0) continue;
-                    value = prop * v0 + (1.0 - prop) * v1;
-
-                    if (m_colourScale != PhaseColourScale) {
-                        if (m_normalization != NormalizeColumns &&
-                            m_normalization != NormalizeHybrid) {
-                            value /= (m_fftSize/2.0);
-                        }
-                        mag.sample(float(value));
-                        value *= m_gain;
-                    }
-
-                    peaks[y] = float(value);
-
-                } else {                    
-
-                    int by0 = int(sy0 + 0.0001);
-                    int by1 = int(sy1 + 0.0001);
-                    if (by1 < by0 + 1) by1 = by0 + 1;
-
-                    for (int bin = by0; bin < by1; ++bin) {
-
-                        value = values[bin - minbin];
-                        if (m_binDisplay == PeakBins) {
-                            if (bin == minbin || bin == maxbin ||
-                                value < values[bin-minbin-1] ||
-                                value < values[bin-minbin+1]) continue;
-                        }
-
-                        if (m_colourScale != PhaseColourScale) {
-                            if (m_normalization != NormalizeColumns &&
-                                m_normalization != NormalizeHybrid) {
-                                value /= (m_fftSize/2.0);
-                            }
-                            mag.sample(float(value));
-                            value *= m_gain;
-                        }
-
-                        if (value > peaks[y]) {
-                            peaks[y] = float(value); //!!! not right for phase!
-                        }
-                    }
-                }
-            }
-
-            if (mag.isSet()) {
-                if (sx >= int(m_columnMags.size())) {
-#ifdef DEBUG_SPECTROGRAM
-                    cerr << "INTERNAL ERROR: " << sx << " >= "
-                              << m_columnMags.size()
-                              << " at SpectrogramLayer.cpp::paintDrawBuffer"
-                              << endl;
-#endif
-                } else {
-                    m_columnMags[sx].sample(mag);
-                    if (overallMag.sample(mag)) overallMagChanged = true;
-                }
-            }
-        }
-
-        for (int y = 0; y < h; ++y) {
-
-            double peak = peaks[y];
-            
-            if (m_colourScale != PhaseColourScale &&
-                (m_normalization == NormalizeColumns ||
-                 m_normalization == NormalizeHybrid) &&
-                columnMax > 0.f) {
-                peak /= columnMax;
-                if (m_normalization == NormalizeHybrid) {
-                    peak *= log10(columnMax + 1.f);
-                }
-            }
-            
-            unsigned char peakpix = getDisplayValue(v, peak);
-
-            m_drawBuffer.setPixel(x, h-y-1, peakpix);
-        }
-    }
-
-    return true;
 }
 
 void
@@ -2715,8 +1580,10 @@
         return;
     }
 
-//    cerr << "SpectrogramLayer: illuminateLocalFeatures("
-//              << localPos.x() << "," << localPos.y() << ")" << endl;
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+    cerr << "SpectrogramLayer: illuminateLocalFeatures("
+              << localPos.x() << "," << localPos.y() << ")" << endl;
+#endif
 
     double s0, s1;
     double f0, f1;
@@ -2733,8 +1600,10 @@
         int y1 = int(getYForFrequency(v, f1));
         int y0 = int(getYForFrequency(v, f0));
         
-//        cerr << "SpectrogramLayer: illuminate "
-//                  << x0 << "," << y1 << " -> " << x1 << "," << y0 << endl;
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+        cerr << "SpectrogramLayer: illuminate "
+                  << x0 << "," << y1 << " -> " << x1 << "," << y0 << endl;
+#endif
         
         paint.setPen(v->getForeground());
 
@@ -2750,7 +1619,7 @@
     return v->getYForFrequency(frequency,
 			       getEffectiveMinFrequency(),
 			       getEffectiveMaxFrequency(),
-			       m_frequencyScale == LogFrequencyScale);
+			       m_binScale == BinScale::Log);
 }
 
 double
@@ -2759,17 +1628,14 @@
     return v->getFrequencyForY(y,
 			       getEffectiveMinFrequency(),
 			       getEffectiveMaxFrequency(),
-			       m_frequencyScale == LogFrequencyScale);
+			       m_binScale == BinScale::Log);
 }
 
 int
-SpectrogramLayer::getCompletion(LayerGeometryProvider *v) const
+SpectrogramLayer::getCompletion(LayerGeometryProvider *) const
 {
-    const View *view = v->getView();
-    
-    if (m_fftModels.find(view) == m_fftModels.end()) return 100;
-
-    int completion = m_fftModels[view]->getCompletion();
+    if (!m_fftModel) return 100;
+    int completion = m_fftModel->getCompletion();
 #ifdef DEBUG_SPECTROGRAM_REPAINT
     cerr << "SpectrogramLayer::getCompletion: completion = " << completion << endl;
 #endif
@@ -2777,11 +1643,10 @@
 }
 
 QString
-SpectrogramLayer::getError(LayerGeometryProvider *v) const
+SpectrogramLayer::getError(LayerGeometryProvider *) const
 {
-    const View *view = v->getView();
-    if (m_fftModels.find(view) == m_fftModels.end()) return "";
-    return m_fftModels[view]->getError();
+    if (!m_fftModel) return "";
+    return m_fftModel->getError();
 }
 
 bool
@@ -2791,10 +1656,10 @@
     if (!m_model) return false;
 
     sv_samplerate_t sr = m_model->getSampleRate();
-    min = double(sr) / m_fftSize;
+    min = double(sr) / getFFTSize();
     max = double(sr) / 2;
     
-    logarithmic = (m_frequencyScale == LogFrequencyScale);
+    logarithmic = (m_binScale == BinScale::Log);
     unit = "Hz";
     return true;
 }
@@ -2824,7 +1689,7 @@
 
     if (m_minFrequency == minf && m_maxFrequency == maxf) return true;
 
-    invalidateImageCaches();
+    invalidateRenderers();
     invalidateMagnitudes();
 
     m_minFrequency = minf;
@@ -2876,16 +1741,10 @@
 void
 SpectrogramLayer::measureDoubleClick(LayerGeometryProvider *v, QMouseEvent *e)
 {
-    const View *view = v->getView();
-    ImageCache &cache = m_imageCaches[view];
-
-    cerr << "cache width: " << cache.image.width() << ", height: "
-         << cache.image.height() << endl;
-
-    QImage image = cache.image;
-
-    ImageRegionFinder finder;
-    QRect rect = finder.findRegionExtents(&image, e->pos());
+    const Colour3DPlotRenderer *renderer = getRenderer(v);
+    if (!renderer) return;
+
+    QRect rect = renderer->findSimilarRegionExtents(e->pos());
     if (rect.isValid()) {
         MeasureRect mr;
         setMeasureRectFromPixrect(v, mr, rect);
@@ -2897,7 +1756,7 @@
 bool
 SpectrogramLayer::getCrosshairExtents(LayerGeometryProvider *v, QPainter &paint,
                                       QPoint cursorPos,
-                                      std::vector<QRect> &extents) const
+                                      vector<QRect> &extents) const
 {
     QRect vertical(cursorPos.x() - 12, 0, 12, v->getPaintHeight());
     extents.push_back(vertical);
@@ -2953,35 +1812,35 @@
     
     double fundamental = getFrequencyForY(v, cursorPos.y());
 
-    v->drawVisibleText(paint,
+    PaintAssistant::drawVisibleText(v, paint,
                        sw + 2,
                        cursorPos.y() - 2,
                        QString("%1 Hz").arg(fundamental),
-                       View::OutlinedText);
+                       PaintAssistant::OutlinedText);
 
     if (Pitch::isFrequencyInMidiRange(fundamental)) {
         QString pitchLabel = Pitch::getPitchLabelForFrequency(fundamental);
-        v->drawVisibleText(paint,
+        PaintAssistant::drawVisibleText(v, paint,
                            sw + 2,
                            cursorPos.y() + paint.fontMetrics().ascent() + 2,
                            pitchLabel,
-                           View::OutlinedText);
+                           PaintAssistant::OutlinedText);
     }
 
     sv_frame_t frame = v->getFrameForX(cursorPos.x());
     RealTime rt = RealTime::frame2RealTime(frame, m_model->getSampleRate());
     QString rtLabel = QString("%1 s").arg(rt.toText(true).c_str());
     QString frameLabel = QString("%1").arg(frame);
-    v->drawVisibleText(paint,
+    PaintAssistant::drawVisibleText(v, paint,
                        cursorPos.x() - paint.fontMetrics().width(frameLabel) - 2,
                        v->getPaintHeight() - 2,
                        frameLabel,
-                       View::OutlinedText);
-    v->drawVisibleText(paint,
+                       PaintAssistant::OutlinedText);
+    PaintAssistant::drawVisibleText(v, paint,
                        cursorPos.x() + 2,
                        v->getPaintHeight() - 2,
                        rtLabel,
-                       View::OutlinedText);
+                       PaintAssistant::OutlinedText);
 
     int harmonic = 2;
 
@@ -3037,7 +1896,7 @@
 
     QString adjFreqText = "", adjPitchText = "";
 
-    if (m_binDisplay == PeakFrequencies) {
+    if (m_binDisplay == BinDisplay::PeakFrequencies) {
 
 	if (!getAdjustedYBinSourceRange(v, x, y, freqMin, freqMax,
 					adjFreqMin, adjFreqMax)) {
@@ -3099,12 +1958,12 @@
 	QString dbMinString;
 	QString dbMaxString;
 	if (dbMin == AudioLevel::DB_FLOOR) {
-	    dbMinString = tr("-Inf");
+	    dbMinString = Strings::minus_infinity;
 	} else {
 	    dbMinString = QString("%1").arg(lrint(dbMin));
 	}
 	if (dbMax == AudioLevel::DB_FLOOR) {
-	    dbMaxString = tr("-Inf");
+	    dbMaxString = Strings::minus_infinity;
 	} else {
 	    dbMaxString = QString("%1").arg(lrint(dbMax));
 	}
@@ -3149,13 +2008,14 @@
     int fw = paint.fontMetrics().width(tr("43Hz"));
     if (tw < fw) tw = fw;
 
-    int tickw = (m_frequencyScale == LogFrequencyScale ? 10 : 4);
+    int tickw = (m_binScale == BinScale::Log ? 10 : 4);
     
     return cw + tickw + tw + 13;
 }
 
 void
-SpectrogramLayer::paintVerticalScale(LayerGeometryProvider *v, bool detailed, QPainter &paint, QRect rect) const
+SpectrogramLayer::paintVerticalScale(LayerGeometryProvider *v, bool detailed,
+                                     QPainter &paint, QRect rect) const
 {
     if (!m_model || !m_model->isOK()) {
 	return;
@@ -3164,107 +2024,32 @@
     Profiler profiler("SpectrogramLayer::paintVerticalScale");
 
     //!!! cache this?
-
+    
     int h = rect.height(), w = rect.width();
-
-    int tickw = (m_frequencyScale == LogFrequencyScale ? 10 : 4);
-    int pkw = (m_frequencyScale == LogFrequencyScale ? 10 : 0);
-
-    int bins = m_fftSize / 2;
+    int textHeight = paint.fontMetrics().height();
+
+    if (detailed && (h > textHeight * 3 + 10)) {
+        paintDetailedScale(v, paint, rect);
+    }
+    m_haveDetailedScale = detailed;
+
+    int tickw = (m_binScale == BinScale::Log ? 10 : 4);
+    int pkw = (m_binScale == BinScale::Log ? 10 : 0);
+
+    int bins = getFFTSize() / 2;
     sv_samplerate_t sr = m_model->getSampleRate();
 
     if (m_maxFrequency > 0) {
-	bins = int((double(m_maxFrequency) * m_fftSize) / sr + 0.1);
-	if (bins > m_fftSize / 2) bins = m_fftSize / 2;
+	bins = int((double(m_maxFrequency) * getFFTSize()) / sr + 0.1);
+	if (bins > getFFTSize() / 2) bins = getFFTSize() / 2;
     }
 
     int cw = 0;
-
     if (detailed) cw = getColourScaleWidth(paint);
-    int cbw = paint.fontMetrics().width("dB");
 
     int py = -1;
-    int textHeight = paint.fontMetrics().height();
     int toff = -textHeight + paint.fontMetrics().ascent() + 2;
 
-    if (detailed && (h > textHeight * 3 + 10)) {
-
-        int topLines = 2;
-        if (m_colourScale == PhaseColourScale) topLines = 1;
-
-	int ch = h - textHeight * (topLines + 1) - 8;
-//	paint.drawRect(4, textHeight + 4, cw - 1, ch + 1);
-	paint.drawRect(4 + cw - cbw, textHeight * topLines + 4, cbw - 1, ch + 1);
-
-	QString top, bottom;
-        double min = m_viewMags[v].getMin();
-        double max = m_viewMags[v].getMax();
-
-        double dBmin = AudioLevel::multiplier_to_dB(min);
-        double dBmax = AudioLevel::multiplier_to_dB(max);
-
-        if (dBmax < -60.f) dBmax = -60.f;
-        else top = QString("%1").arg(lrint(dBmax));
-
-        if (dBmin < dBmax - 60.f) dBmin = dBmax - 60.f;
-        bottom = QString("%1").arg(lrint(dBmin));
-
-        //!!! & phase etc
-
-        if (m_colourScale != PhaseColourScale) {
-            paint.drawText((cw + 6 - paint.fontMetrics().width("dBFS")) / 2,
-                           2 + textHeight + toff, "dBFS");
-        }
-
-//	paint.drawText((cw + 6 - paint.fontMetrics().width(top)) / 2,
-	paint.drawText(3 + cw - cbw - paint.fontMetrics().width(top),
-		       2 + textHeight * topLines + toff + textHeight/2, top);
-
-	paint.drawText(3 + cw - cbw - paint.fontMetrics().width(bottom),
-		       h + toff - 3 - textHeight/2, bottom);
-
-	paint.save();
-	paint.setBrush(Qt::NoBrush);
-
-        int lasty = 0;
-        int lastdb = 0;
-
-	for (int i = 0; i < ch; ++i) {
-
-            double dBval = dBmin + (((dBmax - dBmin) * i) / (ch - 1));
-            int idb = int(dBval);
-
-            double value = AudioLevel::dB_to_multiplier(dBval);
-            int colour = getDisplayValue(v, value * m_gain);
-
-	    paint.setPen(m_palette.getColour((unsigned char)colour));
-
-            int y = textHeight * topLines + 4 + ch - i;
-
-            paint.drawLine(5 + cw - cbw, y, cw + 2, y);
-
-            if (i == 0) {
-                lasty = y;
-                lastdb = idb;
-            } else if (i < ch - paint.fontMetrics().ascent() &&
-                       idb != lastdb &&
-                       ((abs(y - lasty) > textHeight && 
-                         idb % 10 == 0) ||
-                        (abs(y - lasty) > paint.fontMetrics().ascent() && 
-                         idb % 5 == 0))) {
-                paint.setPen(v->getBackground());
-                QString text = QString("%1").arg(idb);
-                paint.drawText(3 + cw - cbw - paint.fontMetrics().width(text),
-                               y + toff + textHeight/2, text);
-                paint.setPen(v->getForeground());
-                paint.drawLine(5 + cw - cbw, y, 8 + cw - cbw, y);
-                lasty = y;
-                lastdb = idb;
-            }
-	}
-	paint.restore();
-    }
-
     paint.drawLine(cw + 7, 0, cw + 7, h);
 
     int bin = -1;
@@ -3283,10 +2068,10 @@
 	    continue;
 	}
 
-	int freq = int((sr * bin) / m_fftSize);
+	int freq = int((sr * bin) / getFFTSize());
 
 	if (py >= 0 && (vy - py) < textHeight - 1) {
-	    if (m_frequencyScale == LinearFrequencyScale) {
+	    if (m_binScale == BinScale::Linear) {
 		paint.drawLine(w - tickw, h - vy, w, h - vy);
 	    }
 	    continue;
@@ -3297,14 +2082,14 @@
 	paint.drawLine(cw + 7, h - vy, w - pkw - 1, h - vy);
 
 	if (h - vy - textHeight >= -2) {
-	    int tx = w - 3 - paint.fontMetrics().width(text) - std::max(tickw, pkw);
+	    int tx = w - 3 - paint.fontMetrics().width(text) - max(tickw, pkw);
 	    paint.drawText(tx, h - vy + toff, text);
 	}
 
 	py = vy;
     }
 
-    if (m_frequencyScale == LogFrequencyScale) {
+    if (m_binScale == BinScale::Log) {
 
         // piano keyboard
 
@@ -3316,6 +2101,152 @@
     m_haveDetailedScale = detailed;
 }
 
+void
+SpectrogramLayer::paintDetailedScale(LayerGeometryProvider *v,
+                                     QPainter &paint, QRect rect) const
+{
+    // The colour scale
+
+    if (m_colourScale == ColourScaleType::Phase) {
+        paintDetailedScalePhase(v, paint, rect);
+        return;
+    }
+    
+    int h = rect.height();
+    int textHeight = paint.fontMetrics().height();
+    int toff = -textHeight + paint.fontMetrics().ascent() + 2;
+
+    int cw = getColourScaleWidth(paint);
+    int cbw = paint.fontMetrics().width("dB");
+
+    int topLines = 2;
+
+    int ch = h - textHeight * (topLines + 1) - 8;
+//	paint.drawRect(4, textHeight + 4, cw - 1, ch + 1);
+    paint.drawRect(4 + cw - cbw, textHeight * topLines + 4, cbw - 1, ch + 1);
+
+    QString top, bottom;
+    double min = m_viewMags[v->getId()].getMin();
+    double max = m_viewMags[v->getId()].getMax();
+
+    if (min < m_threshold) min = m_threshold;
+    if (max <= min) max = min + 0.1;
+        
+    double dBmin = AudioLevel::multiplier_to_dB(min);
+    double dBmax = AudioLevel::multiplier_to_dB(max);
+
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+    cerr << "paintVerticalScale: for view id " << v->getId()
+         << ": min = " << min << ", max = " << max
+         << ", dBmin = " << dBmin << ", dBmax = " << dBmax << endl;
+#endif
+        
+    if (dBmax < -60.f) dBmax = -60.f;
+    else top = QString("%1").arg(lrint(dBmax));
+
+    if (dBmin < dBmax - 60.f) dBmin = dBmax - 60.f;
+    bottom = QString("%1").arg(lrint(dBmin));
+
+#ifdef DEBUG_SPECTROGRAM_REPAINT
+    cerr << "adjusted dB range to min = " << dBmin << ", max = " << dBmax
+         << endl;
+#endif
+        
+    paint.drawText((cw + 6 - paint.fontMetrics().width("dBFS")) / 2,
+                   2 + textHeight + toff, "dBFS");
+
+    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(top),
+                   2 + textHeight * topLines + toff + textHeight/2, top);
+
+    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(bottom),
+                   h + toff - 3 - textHeight/2, bottom);
+
+    paint.save();
+    paint.setBrush(Qt::NoBrush);
+
+    int lasty = 0;
+    int lastdb = 0;
+
+    for (int i = 0; i < ch; ++i) {
+
+        double dBval = dBmin + (((dBmax - dBmin) * i) / (ch - 1));
+        int idb = int(dBval);
+
+        double value = AudioLevel::dB_to_multiplier(dBval);
+        paint.setPen(getRenderer(v)->getColour(value));
+
+        int y = textHeight * topLines + 4 + ch - i;
+
+        paint.drawLine(5 + cw - cbw, y, cw + 2, y);
+        
+        if (i == 0) {
+            lasty = y;
+            lastdb = idb;
+        } else if (i < ch - paint.fontMetrics().ascent() &&
+                   idb != lastdb &&
+                   ((abs(y - lasty) > textHeight && 
+                     idb % 10 == 0) ||
+                    (abs(y - lasty) > paint.fontMetrics().ascent() && 
+                     idb % 5 == 0))) {
+            paint.setPen(v->getForeground());
+            QString text = QString("%1").arg(idb);
+            paint.drawText(3 + cw - cbw - paint.fontMetrics().width(text),
+                           y + toff + textHeight/2, text);
+            paint.drawLine(5 + cw - cbw, y, 8 + cw - cbw, y);
+            lasty = y;
+            lastdb = idb;
+        }
+    }
+    paint.restore();
+}
+
+void
+SpectrogramLayer::paintDetailedScalePhase(LayerGeometryProvider *v,
+                                          QPainter &paint, QRect rect) const
+{
+    // The colour scale in phase mode
+    
+    int h = rect.height();
+    int textHeight = paint.fontMetrics().height();
+    int toff = -textHeight + paint.fontMetrics().ascent() + 2;
+
+    int cw = getColourScaleWidth(paint);
+
+    // Phase is not measured in dB of course, but this places the
+    // scale at the same position as in the magnitude spectrogram
+    int cbw = paint.fontMetrics().width("dB");
+
+    int topLines = 1;
+
+    int ch = h - textHeight * (topLines + 1) - 8;
+    paint.drawRect(4 + cw - cbw, textHeight * topLines + 4, cbw - 1, ch + 1);
+
+    QString top = Strings::pi, bottom = Strings::minus_pi, middle = "0";
+    
+    double min = -M_PI;
+    double max =  M_PI;
+
+    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(top),
+                   2 + textHeight * topLines + toff + textHeight/2, top);
+
+    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(middle),
+                   2 + textHeight * topLines + ch/2 + toff + textHeight/2, middle);
+
+    paint.drawText(3 + cw - cbw - paint.fontMetrics().width(bottom),
+                   h + toff - 3 - textHeight/2, bottom);
+
+    paint.save();
+    paint.setBrush(Qt::NoBrush);
+
+    for (int i = 0; i < ch; ++i) {
+        double val = min + (((max - min) * i) / (ch - 1));
+        paint.setPen(getRenderer(v)->getColour(val));
+        int y = textHeight * topLines + 4 + ch - i;
+        paint.drawLine(5 + cw - cbw, y, cw + 2, y);
+    }
+    paint.restore();
+}
+
 class SpectrogramRangeMapper : public RangeMapper
 {
 public:
@@ -3380,9 +2311,9 @@
 
     sv_samplerate_t sr = m_model->getSampleRate();
 
-    SpectrogramRangeMapper mapper(sr, m_fftSize);
-
-//    int maxStep = mapper.getPositionForValue((double(sr) / m_fftSize) + 0.001);
+    SpectrogramRangeMapper mapper(sr, getFFTSize());
+
+//    int maxStep = mapper.getPositionForValue((double(sr) / getFFTSize()) + 0.001);
     int maxStep = mapper.getPositionForValue(0);
     int minStep = mapper.getPositionForValue(double(sr) / 2);
 
@@ -3404,7 +2335,7 @@
     double dmin, dmax;
     getDisplayExtents(dmin, dmax);
     
-    SpectrogramRangeMapper mapper(m_model->getSampleRate(), m_fftSize);
+    SpectrogramRangeMapper mapper(m_model->getSampleRate(), getFFTSize());
     int n = mapper.getPositionForValue(dmax - dmin);
 //    SVDEBUG << "SpectrogramLayer::getCurrentVerticalZoomStep: " << n << endl;
     return n;
@@ -3421,12 +2352,12 @@
 //    cerr << "current range " << dmin << " -> " << dmax << ", range " << dmax-dmin << ", mid " << (dmax + dmin)/2 << endl;
     
     sv_samplerate_t sr = m_model->getSampleRate();
-    SpectrogramRangeMapper mapper(sr, m_fftSize);
+    SpectrogramRangeMapper mapper(sr, getFFTSize());
     double newdist = mapper.getValueForPosition(step);
 
     double newmin, newmax;
 
-    if (m_frequencyScale == LogFrequencyScale) {
+    if (m_binScale == BinScale::Log) {
 
         // need to pick newmin and newmax such that
         //
@@ -3482,7 +2413,7 @@
 SpectrogramLayer::getNewVerticalZoomRangeMapper() const
 {
     if (!m_model) return 0;
-    return new SpectrogramRangeMapper(m_model->getSampleRate(), m_fftSize);
+    return new SpectrogramRangeMapper(m_model->getSampleRate(), getFFTSize());
 }
 
 void
@@ -3538,11 +2469,11 @@
 		 "binDisplay=\"%7\" ")
 	.arg(m_minFrequency)
 	.arg(m_maxFrequency)
-	.arg(m_colourScale)
+	.arg(convertFromColourScale(m_colourScale, m_colourScaleMultiple))
 	.arg(m_colourMap)
 	.arg(m_colourRotation)
-	.arg(m_frequencyScale)
-	.arg(m_binDisplay);
+	.arg(int(m_binScale))
+	.arg(int(m_binDisplay));
 
     // New-style normalization attributes, allowing for more types of
     // normalization in future: write out the column normalization
@@ -3550,8 +2481,8 @@
     // area as well afterwards
     
     s += QString("columnNormalization=\"%1\" ")
-        .arg(m_normalization == NormalizeColumns ? "peak" :
-             m_normalization == NormalizeHybrid ? "hybrid" : "none");
+        .arg(m_normalization == ColumnNormalization::Max1 ? "peak" :
+             m_normalization == ColumnNormalization::Hybrid ? "hybrid" : "none");
 
     // Old-style normalization attribute. We *don't* write out
     // normalizeHybrid here because the only release that would accept
@@ -3560,12 +2491,12 @@
     // v2.0+ will look odd in Tony v1.0
     
     s += QString("normalizeColumns=\"%1\" ")
-	.arg(m_normalization == NormalizeColumns ? "true" : "false");
+	.arg(m_normalization == ColumnNormalization::Max1 ? "true" : "false");
 
     // And this applies to both old- and new-style attributes
     
     s += QString("normalizeVisibleArea=\"%1\" ")
-        .arg(m_normalization == NormalizeVisibleArea ? "true" : "false");
+        .arg(m_normalizeVisibleArea ? "true" : "false");
     
     Layer::toXml(stream, indent, extraAttributes + " " + s);
 }
@@ -3613,9 +2544,12 @@
         setMaxFrequency(maxFrequency);
     }
 
-    ColourScale colourScale = (ColourScale)
-	attributes.value("colourScale").toInt(&ok);
-    if (ok) setColourScale(colourScale);
+    auto colourScale = convertToColourScale
+        (attributes.value("colourScale").toInt(&ok));
+    if (ok) {
+        setColourScale(colourScale.first);
+        setColourScaleMultiple(colourScale.second);
+    }
 
     int colourMap = attributes.value("colourScheme").toInt(&ok);
     if (ok) setColourMap(colourMap);
@@ -3623,9 +2557,9 @@
     int colourRotation = attributes.value("colourRotation").toInt(&ok);
     if (ok) setColourRotation(colourRotation);
 
-    FrequencyScale frequencyScale = (FrequencyScale)
+    BinScale binScale = (BinScale)
 	attributes.value("frequencyScale").toInt(&ok);
-    if (ok) setFrequencyScale(frequencyScale);
+    if (ok) setBinScale(binScale);
 
     BinDisplay binDisplay = (BinDisplay)
 	attributes.value("binDisplay").toInt(&ok);
@@ -3640,11 +2574,11 @@
         haveNewStyleNormalization = true;
 
         if (columnNormalization == "peak") {
-            setNormalization(NormalizeColumns);
+            setNormalization(ColumnNormalization::Max1);
         } else if (columnNormalization == "hybrid") {
-            setNormalization(NormalizeHybrid);
+            setNormalization(ColumnNormalization::Hybrid);
         } else if (columnNormalization == "none") {
-            // do nothing
+            setNormalization(ColumnNormalization::None);
         } else {
             cerr << "NOTE: Unknown or unsupported columnNormalization attribute \""
                  << columnNormalization << "\"" << endl;
@@ -3656,29 +2590,27 @@
         bool normalizeColumns =
             (attributes.value("normalizeColumns").trimmed() == "true");
         if (normalizeColumns) {
-            setNormalization(NormalizeColumns);
+            setNormalization(ColumnNormalization::Max1);
         }
 
         bool normalizeHybrid =
             (attributes.value("normalizeHybrid").trimmed() == "true");
         if (normalizeHybrid) {
-            setNormalization(NormalizeHybrid);
+            setNormalization(ColumnNormalization::Hybrid);
         }
     }
 
     bool normalizeVisibleArea =
-	(attributes.value("normalizeVisibleArea").trimmed() == "true");
-    if (normalizeVisibleArea) {
-        setNormalization(NormalizeVisibleArea);
-    }
-
-    if (!haveNewStyleNormalization && m_normalization == NormalizeHybrid) {
+        (attributes.value("normalizeVisibleArea").trimmed() == "true");
+    setNormalizeVisibleArea(normalizeVisibleArea);
+
+    if (!haveNewStyleNormalization && m_normalization == ColumnNormalization::Hybrid) {
         // Tony v1.0 is (and hopefully will remain!) the only released
         // SV-a-like to use old-style attributes when saving sessions
         // that ask for hybrid normalization. It saves them with the
         // wrong gain factor, so hack in a fix for that here -- this
         // gives us backward but not forward compatibility.
-        setGain(m_gain / float(m_fftSize / 2));
+        setGain(m_gain / float(getFFTSize() / 2));
     }
 }
     
--- a/layer/SpectrogramLayer.h	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/SpectrogramLayer.h	Fri Aug 19 15:58:57 2016 +0100
@@ -13,11 +13,12 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _SPECTROGRAM_LAYER_H_
-#define _SPECTROGRAM_LAYER_H_
+#ifndef SPECTROGRAM_LAYER_H
+#define SPECTROGRAM_LAYER_H
 
 #include "SliceableLayer.h"
 #include "base/Window.h"
+#include "base/MagnitudeRange.h"
 #include "base/RealTime.h"
 #include "base/Thread.h"
 #include "base/PropertyContainer.h"
@@ -25,6 +26,10 @@
 #include "data/model/DenseTimeValueModel.h"
 #include "data/model/FFTModel.h"
 
+#include "VerticalBinLayer.h"
+#include "ColourScale.h"
+#include "Colour3DPlotRenderer.h"
+
 #include <QMutex>
 #include <QWaitCondition>
 #include <QImage>
@@ -38,13 +43,12 @@
 class FFTModel;
 class Dense3DModelPeakCache;
 
-
 /**
  * SpectrogramLayer represents waveform data (obtained from a
  * DenseTimeValueModel) in spectrogram form.
  */
 
-class SpectrogramLayer : public SliceableLayer,
+class SpectrogramLayer : public VerticalBinLayer,
 			 public PowerOfSqrtTwoZoomConstraint
 {
     Q_OBJECT
@@ -110,9 +114,6 @@
     void setWindowType(WindowType type);
     WindowType getWindowType() const;
 
-    void setZeroPadLevel(int level);
-    int getZeroPadLevel() const;
-
     /**
      * Set the gain multiplier for sample values in this view.
      * The default is 1.0.
@@ -124,7 +125,7 @@
      * Set the threshold for sample values to qualify for being shown
      * in the FFT, in voltage units.
      *
-     * The default is 0.0.
+     * The default is 10^-8 (-80dB).
      */
     void setThreshold(float threshold);
     float getThreshold() const;
@@ -135,56 +136,44 @@
     void setMaxFrequency(int); // 0 -> no maximum
     int getMaxFrequency() const;
 
-    enum ColourScale {
-	LinearColourScale,
-	MeterColourScale,
-        dBSquaredColourScale,
-	dBColourScale,
-	PhaseColourScale
-    };
+    /**
+     * Specify the scale for sample levels.  See ColourScale and
+     * WaveformLayer for comparison and details of meter and dB
+     * scaling.  The default is LogColourScale.
+     */
+    void setColourScale(ColourScaleType);
+    ColourScaleType getColourScale() const;
 
     /**
-     * Specify the scale for sample levels.  See WaveformLayer for
-     * details of meter and dB scaling.  The default is dBColourScale.
+     * Specify multiple factor for colour scale. This is 2.0 for
+     * log-power spectrogram and 1.0 otherwise.
      */
-    void setColourScale(ColourScale);
-    ColourScale getColourScale() const;
-
-    enum FrequencyScale {
-	LinearFrequencyScale,
-	LogFrequencyScale
-    };
+    void setColourScaleMultiple(double);
+    double getColourScaleMultiple() const;
     
     /**
      * Specify the scale for the y axis.
      */
-    void setFrequencyScale(FrequencyScale);
-    FrequencyScale getFrequencyScale() const;
+    void setBinScale(BinScale);
+    BinScale getBinScale() const;
 
-    enum BinDisplay {
-	AllBins,
-	PeakBins,
-	PeakFrequencies
-    };
-    
     /**
      * Specify the processing of frequency bins for the y axis.
      */
     void setBinDisplay(BinDisplay);
     BinDisplay getBinDisplay() const;
 
-    enum Normalization {
-        NoNormalization,
-        NormalizeColumns,
-        NormalizeVisibleArea,
-        NormalizeHybrid
-    };
+    /**
+     * Specify the normalization mode for individual columns.
+     */
+    void setNormalization(ColumnNormalization);
+    ColumnNormalization getNormalization() const;
 
     /**
-     * Specify the normalization mode for bin values.
+     * Specify whether to normalize the visible area.
      */
-    void setNormalization(Normalization);
-    Normalization getNormalization() const;
+    void setNormalizeVisibleArea(bool);
+    bool getNormalizeVisibleArea() const;
 
     /**
      * Specify the colour map. See ColourMapper for the colour map
@@ -212,6 +201,10 @@
     double getYForFrequency(const LayerGeometryProvider *v, double frequency) const;
     double getFrequencyForY(const LayerGeometryProvider *v, int y) const;
 
+    //!!! VerticalBinLayer methods. Note overlap with get*BinRange()
+    double getYForBin(const LayerGeometryProvider *, double bin) const;
+    double getBinForY(const LayerGeometryProvider *, double y) const;
+    
     virtual int getCompletion(LayerGeometryProvider *v) const;
     virtual QString getError(LayerGeometryProvider *v) const;
 
@@ -250,84 +243,38 @@
     const DenseTimeValueModel *m_model; // I do not own this
 
     int                 m_channel;
-    int              m_windowSize;
+    int                 m_windowSize;
     WindowType          m_windowType;
-    int              m_windowHopLevel;
-    int              m_zeroPadLevel;
-    int              m_fftSize;
+    int                 m_windowHopLevel;
     float               m_gain;
     float               m_initialGain;
     float               m_threshold;
     float               m_initialThreshold;
     int                 m_colourRotation;
     int                 m_initialRotation;
-    int              m_minFrequency;
-    int              m_maxFrequency;
-    int              m_initialMaxFrequency;
-    ColourScale         m_colourScale;
+    int                 m_minFrequency;
+    int                 m_maxFrequency;
+    int                 m_initialMaxFrequency;
+    ColourScaleType     m_colourScale;
+    double              m_colourScaleMultiple;
     int                 m_colourMap;
     QColor              m_crosshairColour;
-    FrequencyScale      m_frequencyScale;
+    BinScale            m_binScale;
     BinDisplay          m_binDisplay;
-    Normalization       m_normalization;
+    ColumnNormalization m_normalization; // of individual columns
+    bool                m_normalizeVisibleArea;
     int                 m_lastEmittedZoomStep;
     bool                m_synchronous;
 
     mutable bool        m_haveDetailedScale;
-    mutable int         m_lastPaintBlockWidth;
-    mutable RealTime    m_lastPaintTime;
 
-    enum { NO_VALUE = 0 }; // colour index for unused pixels
-
-    class Palette
-    {
-    public:
-        QColor getColour(unsigned char index) const {
-            return m_colours[index];
-        }
-    
-        void setColour(unsigned char index, QColor colour) {
-            m_colours[index] = colour;
-        }
-
-    private:
-        QColor m_colours[256];
-    };
-
-    Palette m_palette;
-
-    /**
-     * ImageCache covers the area of the view, at view resolution.
-     * Not all of it is necessarily valid at once (it is refreshed
-     * in parts when scrolling, for example).
-     */
-    struct ImageCache
-    {
-        QImage image;
-        QRect validArea;
-        sv_frame_t startFrame;
-        int zoomLevel;
-    };
-    typedef std::map<const View *, ImageCache> ViewImageCache;
-    void invalidateImageCaches();
-    void invalidateImageCaches(sv_frame_t startFrame, sv_frame_t endFrame);
-    mutable ViewImageCache m_imageCaches;
-
-    /**
-     * When painting, we draw directly onto the draw buffer and then
-     * copy this to the part of the image cache that needed refreshing
-     * before copying the image cache onto the window.  (Remind me why
-     * we don't draw directly onto the cache?)
-     */
-    mutable QImage m_drawBuffer;
+    static std::pair<ColourScaleType, double> convertToColourScale(int value);
+    static int convertFromColourScale(ColourScaleType type, double multiple);
+    static std::pair<ColumnNormalization, bool> convertToColumnNorm(int value);
+    static int convertFromColumnNorm(ColumnNormalization norm, bool visible);
 
     bool m_exiting;
 
-    void initialisePalette();
-    void rotatePalette(int distance);
-
-    unsigned char getDisplayValue(LayerGeometryProvider *v, double input) const;
-
     int getColourScaleWidth(QPainter &) const;
 
     void illuminateLocalFeatures(LayerGeometryProvider *v, QPainter &painter) const;
@@ -335,15 +282,8 @@
     double getEffectiveMinFrequency() const;
     double getEffectiveMaxFrequency() const;
 
-    // Note that the getYBin... methods return the nominal bin in the
-    // un-smoothed spectrogram.  This is not necessarily the same bin
-    // as is pulled from the spectrogram and drawn at the given
-    // position, if the spectrogram has oversampling smoothing.  Use
-    // getSmoothedYBinRange to obtain that.
-
     bool getXBinRange(LayerGeometryProvider *v, int x, double &windowMin, double &windowMax) const;
     bool getYBinRange(LayerGeometryProvider *v, int y, double &freqBinMin, double &freqBinMax) const;
-    bool getSmoothedYBinRange(LayerGeometryProvider *v, int y, double &freqBinMin, double &freqBinMax) const;
 
     bool getYBinSourceRange(LayerGeometryProvider *v, int y, double &freqMin, double &freqMax) const;
     bool getAdjustedYBinSourceRange(LayerGeometryProvider *v, int x, int y,
@@ -359,83 +299,38 @@
         else return m_windowSize / (1 << (m_windowHopLevel - 1));
     }
 
-    int getZeroPadLevel(const LayerGeometryProvider *v) const;
-    int getFFTSize(const LayerGeometryProvider *v) const;
-    FFTModel *getFFTModel(const LayerGeometryProvider *v) const;
-    Dense3DModelPeakCache *getPeakCache(const LayerGeometryProvider *v) const;
-    void invalidateFFTModels();
+    int getFFTOversampling() const;
+    int getFFTSize() const; // m_windowSize * getFFTOversampling()
 
-    typedef std::map<const View *, FFTModel *> ViewFFTMap;
-    typedef std::map<const View *, Dense3DModelPeakCache *> PeakCacheMap;
-    mutable ViewFFTMap m_fftModels;
-    mutable PeakCacheMap m_peakCaches;
-    mutable Model *m_sliceableModel;
+    mutable FFTModel *m_fftModel; //!!! should not be mutable, see getFFTModel()?
+    mutable Dense3DModelPeakCache *m_peakCache;
+    const int m_peakCacheDivisor;
 
-    class MagnitudeRange {
-    public:
-        MagnitudeRange() : m_min(0), m_max(0) { }
-        bool operator==(const MagnitudeRange &r) {
-            return r.m_min == m_min && r.m_max == m_max;
-        }
-        bool isSet() const { return (m_min != 0.f || m_max != 0.f); }
-        void set(float min, float max) {
-            m_min = min;
-            m_max = max;
-            if (m_max < m_min) m_max = m_min;
-        }
-        bool sample(float f) {
-            bool changed = false;
-            if (isSet()) {
-                if (f < m_min) { m_min = f; changed = true; }
-                if (f > m_max) { m_max = f; changed = true; }
-            } else {
-                m_max = m_min = f;
-                changed = true;
-            }
-            return changed;
-        }            
-        bool sample(const MagnitudeRange &r) {
-            bool changed = false;
-            if (isSet()) {
-                if (r.m_min < m_min) { m_min = r.m_min; changed = true; }
-                if (r.m_max > m_max) { m_max = r.m_max; changed = true; }
-            } else {
-                m_min = r.m_min;
-                m_max = r.m_max;
-                changed = true;
-            }
-            return changed;
-        }            
-        float getMin() const { return m_min; }
-        float getMax() const { return m_max; }
-    private:
-        float m_min;
-        float m_max;
-    };
+    typedef std::map<int, MagnitudeRange> ViewMagMap; // key is view id
+    mutable ViewMagMap m_viewMags;
+    mutable ViewMagMap m_lastRenderedMags; // when in normalizeVisibleArea mode
+    void invalidateMagnitudes();
 
-    typedef std::map<const LayerGeometryProvider *, MagnitudeRange> ViewMagMap;
-    mutable ViewMagMap m_viewMags;
-    mutable std::vector<MagnitudeRange> m_columnMags;
-    void invalidateMagnitudes();
-    bool updateViewMagnitudes(LayerGeometryProvider *v) const;
-    bool paintDrawBuffer(LayerGeometryProvider *v, int w, int h,
-                         const std::vector<int> &binforx,
-                         const std::vector<double> &binfory,
-                         bool usePeaksCache,
-                         MagnitudeRange &overallMag,
-                         bool &overallMagChanged) const;
-    bool paintDrawBufferPeakFrequencies(LayerGeometryProvider *v, int w, int h,
-                                        const std::vector<int> &binforx,
-                                        int minbin,
-                                        int maxbin,
-                                        double displayMinFreq,
-                                        double displayMaxFreq,
-                                        bool logarithmic,
-                                        MagnitudeRange &overallMag,
-                                        bool &overallMagChanged) const;
+    typedef std::map<int, Colour3DPlotRenderer *> ViewRendererMap; // key is view id
+    mutable ViewRendererMap m_renderers;
+    Colour3DPlotRenderer *getRenderer(LayerGeometryProvider *) const;
+    void invalidateRenderers();
+    
+    FFTModel *getFFTModel() const;
+    Dense3DModelPeakCache *getPeakCache() const;
+    void invalidateFFTModel();
 
-    virtual void updateMeasureRectYCoords(LayerGeometryProvider *v, const MeasureRect &r) const;
-    virtual void setMeasureRectYCoord(LayerGeometryProvider *v, MeasureRect &r, bool start, int y) const;
+    void paintWithRenderer(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
+
+    void paintDetailedScale(LayerGeometryProvider *v,
+                            QPainter &paint, QRect rect) const;
+    void paintDetailedScalePhase(LayerGeometryProvider *v,
+                                 QPainter &paint, QRect rect) const;
+    
+    virtual void updateMeasureRectYCoords(LayerGeometryProvider *v,
+                                          const MeasureRect &r) const;
+    virtual void setMeasureRectYCoord(LayerGeometryProvider *v,
+                                      MeasureRect &r, bool start, int y) const;
 };
 
 #endif
--- a/layer/SpectrumLayer.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/SpectrumLayer.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -21,7 +21,10 @@
 #include "base/Preferences.h"
 #include "base/RangeMapper.h"
 #include "base/Pitch.h"
+#include "base/Strings.h"
+
 #include "ColourMapper.h"
+#include "PaintAssistant.h"
 
 #include <QPainter>
 #include <QTextStream>
@@ -491,19 +494,19 @@
     int hoffset = 2;
     if (m_binScale == LogBins) hoffset = 13;
 
-    v->drawVisibleText(paint,
+    PaintAssistant::drawVisibleText(v, paint,
                        cursorPos.x() + 2,
                        v->getPaintHeight() - 2 - hoffset,
                        QString("%1 Hz").arg(fundamental),
-                       View::OutlinedText);
+                       PaintAssistant::OutlinedText);
 
     if (Pitch::isFrequencyInMidiRange(fundamental)) {
         QString pitchLabel = Pitch::getPitchLabelForFrequency(fundamental);
-        v->drawVisibleText(paint,
+        PaintAssistant::drawVisibleText(v, paint,
                            cursorPos.x() - paint.fontMetrics().width(pitchLabel) - 2,
                            v->getPaintHeight() - 2 - hoffset,
                            pitchLabel,
-                           View::OutlinedText);
+                           PaintAssistant::OutlinedText);
     }
 
     double value = getValueForY(cursorPos.y(), v);
@@ -512,17 +515,17 @@
     if (value > 0.0) db = 10.0 * log10(value);
     if (db < thresh) db = thresh;
 
-    v->drawVisibleText(paint,
+    PaintAssistant::drawVisibleText(v, paint,
                        xorigin + 2,
                        cursorPos.y() - 2,
                        QString("%1 V").arg(value),
-                       View::OutlinedText);
+                       PaintAssistant::OutlinedText);
 
-    v->drawVisibleText(paint,
+    PaintAssistant::drawVisibleText(v, paint,
                        xorigin + 2,
                        cursorPos.y() + 2 + paint.fontMetrics().ascent(),
                        QString("%1 dBV").arg(db),
-                       View::OutlinedText);
+                       PaintAssistant::OutlinedText);
     
     int harmonic = 2;
 
@@ -605,12 +608,12 @@
     QString mindbstr;
     QString maxdbstr;
     if (mindb == AudioLevel::DB_FLOOR) {
-        mindbstr = tr("-Inf");
+        mindbstr = Strings::minus_infinity;
     } else {
         mindbstr = QString("%1").arg(lrint(mindb));
     }
     if (maxdb == AudioLevel::DB_FLOOR) {
-        maxdbstr = tr("-Inf");
+        maxdbstr = Strings::minus_infinity;
     } else {
         maxdbstr = QString("%1").arg(lrint(maxdb));
     }
--- a/layer/TimeInstantLayer.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/TimeInstantLayer.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -20,7 +20,9 @@
 #include "view/View.h"
 #include "base/Profiler.h"
 #include "base/Clipboard.h"
+
 #include "ColourDatabase.h"
+#include "PaintAssistant.h"
 
 #include "data/model/SparseOneDimensionalModel.h"
 
@@ -456,7 +458,7 @@
 	    }
 
 	    if (good) {
-                v->drawVisibleText(paint, x + iw + 2, textY, p.label, View::OutlinedText);
+                PaintAssistant::drawVisibleText(v, paint, x + iw + 2, textY, p.label, PaintAssistant::OutlinedText);
 //		paint.drawText(x + iw + 2, textY, p.label);
 	    }
 	}
--- a/layer/TimeRulerLayer.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/TimeRulerLayer.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -19,19 +19,20 @@
 
 #include "data/model/Model.h"
 #include "base/RealTime.h"
+#include "view/View.h"
+
 #include "ColourDatabase.h"
-#include "view/View.h"
+#include "PaintAssistant.h"
 
 #include <QPainter>
 
 #include <iostream>
 #include <cmath>
+#include <stdexcept>
 
 //#define DEBUG_TIME_RULER_LAYER 1
 
 
-
-
 TimeRulerLayer::TimeRulerLayer() :
     SingleColourLayer(),
     m_model(0),
@@ -182,6 +183,8 @@
     } else {
 	incms = 1;
 	int ms = rtGap.msec();
+//        cerr << "rtGap.msec = " << ms << ", rtGap = " << rtGap << ", count = " << count << endl;
+//        cerr << "startFrame = " << startFrame << ", endFrame = " << endFrame << " rtStart = " << rtStart << ", rtEnd = " << rtEnd << endl;
 	if (ms > 0) { incms *= 10; ms /= 10; }
 	if (ms > 0) { incms *= 10; ms /= 10; }
 	if (ms > 0) { incms *= 5; ms /= 5; }
@@ -241,6 +244,9 @@
     // time < 0 which would cut it in half
     int minlabel = 1; // ms
 
+    // used for a sanity check
+    sv_frame_t prevframe = 0;
+    
     while (1) {
 
         // frame is used to determine where to draw the lines, so it
@@ -253,10 +259,17 @@
         frame /= v->getZoomLevel();
         frame *= v->getZoomLevel(); // so frame corresponds to an exact pixel
 
+        if (frame == prevframe && prevframe != 0) {
+            cerr << "ERROR: frame == prevframe (== " << frame
+                 << ") in TimeRulerLayer::paint" << endl;
+            throw std::logic_error("frame == prevframe in TimeRulerLayer::paint");
+        }
+        prevframe = frame;
+        
         int x = v->getXForFrame(frame);
 
 #ifdef DEBUG_TIME_RULER_LAYER
-        SVDEBUG << "Considering frame = " << frame << ", x = " << x << endl;
+        cerr << "Considering frame = " << frame << ", x = " << x << endl;
 #endif
 
         if (x >= rect.x() + rect.width() + 50) {
@@ -313,7 +326,7 @@
                     // backmost layer, don't worry about outlining the text
                     paint.drawText(x+2 - tw/2, y, text);
                 } else {
-                    v->drawVisibleText(paint, x+2 - tw/2, y, text, View::OutlinedText);
+                    PaintAssistant::drawVisibleText(v, paint, x+2 - tw/2, y, text, PaintAssistant::OutlinedText);
                 }
             }
         }
--- a/layer/TimeValueLayer.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/TimeValueLayer.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -21,7 +21,6 @@
 #include "base/LogRange.h"
 #include "base/RangeMapper.h"
 #include "base/Pitch.h"
-#include "ColourDatabase.h"
 #include "view/View.h"
 
 #include "data/model/SparseTimeValueModel.h"
@@ -31,12 +30,14 @@
 #include "widgets/ListInputDialog.h"
 #include "widgets/TextAbbrev.h"
 
+#include "ColourDatabase.h"
 #include "ColourMapper.h"
 #include "PianoScale.h"
 #include "LinearNumericalScale.h"
 #include "LogNumericalScale.h"
 #include "LinearColourScale.h"
 #include "LogColourScale.h"
+#include "PaintAssistant.h"
 
 #include <QPainter>
 #include <QPainterPath>
@@ -1214,10 +1215,10 @@
                 if (haveRoom ||
                     (!haveNext &&
                      (pointCount == 0 || !italic))) {
-                    v->drawVisibleText(paint, x + 5, textY, label,
+                    PaintAssistant::drawVisibleText(v, paint, x + 5, textY, label,
                                        italic ?
-                                       View::OutlinedItalicText :
-                                       View::OutlinedText);
+                                       PaintAssistant::OutlinedItalicText :
+                                       PaintAssistant::OutlinedText);
                 }
             }
         }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/VerticalBinLayer.h	Fri Aug 19 15:58:57 2016 +0100
@@ -0,0 +1,62 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef VERTICAL_BIN_LAYER_H
+#define VERTICAL_BIN_LAYER_H
+
+#include "SliceableLayer.h"
+
+/**
+ * Interface for layers in which the Y axis corresponds to bin number
+ * rather than scale value. Colour3DPlotLayer is the obvious example.
+ * Conceptually these are always SliceableLayers as well, and this
+ * subclasses from SliceableLayer to avoid a big inheritance mess.
+ */
+class VerticalBinLayer : public SliceableLayer
+{
+public:
+    /**
+     * Return the y coordinate at which the given bin "starts"
+     * (i.e. at the bottom of the bin, if the given bin is an integer
+     * and the vertical scale is the usual way up). Bin number may be
+     * fractional, to obtain a position part-way through a bin.
+     */
+    virtual double getYForBin(const LayerGeometryProvider *, double bin) const = 0;
+
+    /**
+     * As getYForBin, but rounding to integer values.
+     */
+    virtual int getIYForBin(const LayerGeometryProvider *v, int bin) const {
+        return int(round(getYForBin(v, bin)));
+    }
+    
+    /**
+     * Return the bin number, possibly fractional, at the given y
+     * coordinate. Note that the whole numbers occur at the positions
+     * at which the bins "start" (i.e. the bottom of the visible bin,
+     * if the vertical scale is the usual way up).
+     */
+    virtual double getBinForY(const LayerGeometryProvider *, double y) const = 0;
+
+    /**
+     * As getBinForY, but rounding to integer values.
+     */
+    virtual int getIBinForY(const LayerGeometryProvider *v, int y) const {
+        return int(floor(getBinForY(v, y)));
+    }
+};
+
+#endif
+
--- a/layer/VerticalScaleLayer.h	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/VerticalScaleLayer.h	Fri Aug 19 15:58:57 2016 +0100
@@ -16,6 +16,12 @@
 #ifndef VERTICAL_SCALE_LAYER_H
 #define VERTICAL_SCALE_LAYER_H
 
+/**
+ * Interface for layers in which the Y axis represents (or can
+ * sometimes represent, depending on the display mode) the sample
+ * value. For example, TimeValueLayer uses vertical scale when in
+ * point mode and so provides this interface.
+ */
 class VerticalScaleLayer
 {
 public:
--- a/layer/WaveformLayer.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/layer/WaveformLayer.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -19,7 +19,10 @@
 #include "view/View.h"
 #include "base/Profiler.h"
 #include "base/RangeMapper.h"
+#include "base/Strings.h"
+
 #include "ColourDatabase.h"
+#include "PaintAssistant.h"
 
 #include <QPainter>
 #include <QPixmap>
@@ -1201,7 +1204,7 @@
 	return paint.fontMetrics().width("0.0") + 13;
     } else {
 	return std::max(paint.fontMetrics().width(tr("0dB")),
-			paint.fontMetrics().width(tr("-Inf"))) + 13;
+			paint.fontMetrics().width(Strings::minus_infinity)) + 13;
     }
 }
 
@@ -1255,7 +1258,7 @@
 		text = QString("%1").arg(meterdbs[i]);
 		if (i == n) text = tr("0dB");
 		if (i == 0) {
-                    text = tr("-Inf");
+                    text = Strings::minus_infinity;
                     val = 0.0;
 		}
                 break;
@@ -1265,7 +1268,7 @@
 		text = QString("%1").arg(-(10*n) + i * 10);
 		if (i == n) text = tr("0dB");
 		if (i == 0) {
-                    text = tr("-Inf");
+                    text = Strings::minus_infinity;
                     val = 0.0;
 		}
                 break;
--- a/svgui.pro	Tue Oct 20 12:55:09 2015 +0100
+++ b/svgui.pro	Fri Aug 19 15:58:57 2016 +0100
@@ -1,6 +1,9 @@
 
 TEMPLATE = lib
 
+INCLUDEPATH += ../vamp-plugin-sdk
+DEFINES += HAVE_VAMP HAVE_VAMPHOSTSDK
+
 exists(config.pri) {
     include(config.pri)
 }
@@ -22,7 +25,7 @@
         LIBS += -L../sv-dependency-builds/osx/lib
     }
 
-    DEFINES += HAVE_BZ2 HAVE_FFTW3 HAVE_FFTW3F HAVE_SNDFILE HAVE_SAMPLERATE HAVE_VAMP HAVE_VAMPHOSTSDK HAVE_RUBBERBAND HAVE_LIBLO HAVE_MAD HAVE_ID3TAG
+    DEFINES += HAVE_BZ2 HAVE_FFTW3 HAVE_FFTW3F HAVE_SNDFILE HAVE_SAMPLERATE HAVE_RUBBERBAND HAVE_LIBLO HAVE_MAD HAVE_ID3TAG
 }
 
 CONFIG += staticlib qt thread warn_on stl rtti exceptions c++11
@@ -36,14 +39,17 @@
 MOC_DIR = o
 
 HEADERS += layer/Colour3DPlotLayer.h \
+	   layer/Colour3DPlotRenderer.h \
 	   layer/ColourDatabase.h \
 	   layer/ColourMapper.h \
+           layer/ColourScale.h \
            layer/ColourScaleLayer.h \
            layer/FlexiNoteLayer.h \
            layer/ImageLayer.h \
            layer/ImageRegionFinder.h \
            layer/Layer.h \
            layer/LayerFactory.h \
+           layer/LayerGeometryProvider.h \
            layer/LinearNumericalScale.h \
            layer/LogNumericalScale.h \
            layer/LinearColourScale.h \
@@ -52,6 +58,9 @@
            layer/PaintAssistant.h \
            layer/PianoScale.h \
            layer/RegionLayer.h \
+           layer/RenderTimer.h \
+           layer/ScrollableImageCache.h \
+           layer/ScrollableMagRangeCache.h \
            layer/SingleColourLayer.h \
            layer/SliceableLayer.h \
            layer/SliceLayer.h \
@@ -64,8 +73,10 @@
            layer/VerticalScaleLayer.h \
            layer/WaveformLayer.h
 SOURCES += layer/Colour3DPlotLayer.cpp \
+	   layer/Colour3DPlotRenderer.cpp \
 	   layer/ColourDatabase.cpp \
 	   layer/ColourMapper.cpp \
+	   layer/ColourScale.cpp \
            layer/FlexiNoteLayer.cpp \
            layer/ImageLayer.cpp \
            layer/ImageRegionFinder.cpp \
@@ -79,6 +90,8 @@
            layer/PaintAssistant.cpp \
            layer/PianoScale.cpp \
            layer/RegionLayer.cpp \
+           layer/ScrollableImageCache.cpp \
+           layer/ScrollableMagRangeCache.cpp \
            layer/SingleColourLayer.cpp \
            layer/SliceLayer.cpp \
            layer/SpectrogramLayer.cpp \
@@ -89,13 +102,16 @@
            layer/TimeValueLayer.cpp \
            layer/WaveformLayer.cpp
 
-HEADERS += view/Overview.h \
+HEADERS += view/AlignmentView.h \
+           view/Overview.h \
            view/Pane.h \
            view/PaneStack.h \
            view/View.h \
            view/ViewManager.h \
            view/ViewProxy.h
-SOURCES += view/Overview.cpp \
+           
+SOURCES += view/AlignmentView.cpp \
+           view/Overview.cpp \
            view/Pane.cpp \
            view/PaneStack.cpp \
            view/View.cpp \
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/view/AlignmentView.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -0,0 +1,200 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2014 Chris Cannam and QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "AlignmentView.h"
+
+#include <QPainter>
+
+#include "data/model/SparseOneDimensionalModel.h"
+
+#include "layer/TimeInstantLayer.h"
+
+using std::vector;
+
+AlignmentView::AlignmentView(QWidget *w) :
+    View(w, false),
+    m_above(0),
+    m_below(0)
+{
+    setObjectName(tr("AlignmentView"));
+}
+
+void
+AlignmentView::globalCentreFrameChanged(sv_frame_t f)
+{
+    View::globalCentreFrameChanged(f);
+    update();
+}
+
+void
+AlignmentView::viewCentreFrameChanged(View *v, sv_frame_t f)
+{
+    View::viewCentreFrameChanged(v, f);
+    if (v == m_above) {
+	m_centreFrame = f;
+	update();
+    } else if (v == m_below) {
+	update();
+    }
+}
+
+void
+AlignmentView::viewManagerPlaybackFrameChanged(sv_frame_t)
+{
+    update();
+}
+
+void
+AlignmentView::viewAboveZoomLevelChanged(int level, bool)
+{
+    m_zoomLevel = level;
+    update();
+}
+
+void
+AlignmentView::viewBelowZoomLevelChanged(int, bool)
+{
+    update();
+}
+
+void
+AlignmentView::setViewAbove(View *v)
+{
+    if (m_above) {
+	disconnect(m_above, 0, this, 0);
+    }
+
+    m_above = v;
+
+    if (m_above) {
+	connect(m_above,
+		SIGNAL(zoomLevelChanged(int, bool)),
+		this, 
+		SLOT(viewAboveZoomLevelChanged(int, bool)));
+    }
+}
+
+void
+AlignmentView::setViewBelow(View *v)
+{
+    if (m_below) {
+	disconnect(m_below, 0, this, 0);
+    }
+
+    m_below = v;
+
+    if (m_below) {
+	connect(m_below,
+		SIGNAL(zoomLevelChanged(int, bool)),
+		this, 
+		SLOT(viewBelowZoomLevelChanged(int, bool)));
+    }
+}
+
+void
+AlignmentView::paintEvent(QPaintEvent *)
+{
+    if (m_above == 0 || m_below == 0 || !m_manager) return;
+
+    bool darkPalette = false;
+    if (m_manager) darkPalette = m_manager->getGlobalDarkBackground();
+
+    QColor fg, bg;
+    if (darkPalette) {
+        fg = Qt::gray;
+        bg = Qt::black;
+    } else {
+        fg = Qt::black;
+        bg = Qt::gray;
+    }
+
+    QPainter paint(this);
+    paint.setPen(QPen(fg, 2));
+    paint.setBrush(Qt::NoBrush);
+    paint.setRenderHint(QPainter::Antialiasing, true);
+
+    paint.fillRect(rect(), bg);
+
+    vector<sv_frame_t> keyFrames = getKeyFrames();
+
+    foreach (sv_frame_t f, keyFrames) {
+	int ax = m_above->getXForFrame(f);
+	sv_frame_t rf = m_above->alignToReference(f);
+	sv_frame_t bf = m_below->alignFromReference(rf);
+	int bx = m_below->getXForFrame(bf);
+	paint.drawLine(ax, 0, bx, height());
+    }
+
+    paint.end();
+}
+
+vector<sv_frame_t>
+AlignmentView::getKeyFrames()
+{
+    if (!m_above) {
+	return getDefaultKeyFrames();
+    }
+
+    SparseOneDimensionalModel *m = 0;
+
+    // get the topmost such
+    for (int i = 0; i < m_above->getLayerCount(); ++i) {
+	if (qobject_cast<TimeInstantLayer *>(m_above->getLayer(i))) {
+	    SparseOneDimensionalModel *mm = 
+		qobject_cast<SparseOneDimensionalModel *>
+		(m_above->getLayer(i)->getModel());
+	    if (mm) m = mm;
+	}
+    }
+
+    if (!m) {
+	return getDefaultKeyFrames();
+    }
+
+    vector<sv_frame_t> keyFrames;
+
+    const SparseOneDimensionalModel::PointList pp = m->getPoints();
+    for (SparseOneDimensionalModel::PointList::const_iterator pi = pp.begin();
+	 pi != pp.end(); ++pi) {
+	keyFrames.push_back(pi->frame);
+    }
+
+    return keyFrames;
+}
+
+vector<sv_frame_t>
+AlignmentView::getDefaultKeyFrames()
+{
+    vector<sv_frame_t> keyFrames;
+
+    if (!m_above || !m_manager) return keyFrames;
+
+    sv_samplerate_t rate = m_manager->getMainModelSampleRate();
+    if (rate == 0) return keyFrames;
+
+    for (sv_frame_t f = m_above->getModelsStartFrame(); 
+	 f <= m_above->getModelsEndFrame(); 
+	 f += sv_frame_t(rate * 5 + 0.5)) {
+	keyFrames.push_back(f);
+    }
+    
+    return keyFrames;
+}
+
+
+
+
+
+    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/view/AlignmentView.h	Fri Aug 19 15:58:57 2016 +0100
@@ -0,0 +1,50 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2014 Chris Cannam and QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef ALIGNMENT_VIEW_H
+#define ALIGNMENT_VIEW_H
+
+#include "View.h"
+
+class AlignmentView : public View
+{
+    Q_OBJECT
+
+public:
+    AlignmentView(QWidget *parent = 0);
+    virtual QString getPropertyContainerIconName() const { return "alignment"; }
+    
+    void setViewAbove(View *view);
+    void setViewBelow(View *view);
+
+public slots:
+    virtual void globalCentreFrameChanged(sv_frame_t);
+    virtual void viewCentreFrameChanged(View *, sv_frame_t);
+    virtual void viewAboveZoomLevelChanged(int, bool);
+    virtual void viewBelowZoomLevelChanged(int, bool);
+    virtual void viewManagerPlaybackFrameChanged(sv_frame_t);
+
+protected:
+    virtual void paintEvent(QPaintEvent *e);
+    virtual bool shouldLabelSelections() const { return false; }
+
+    std::vector<sv_frame_t> getKeyFrames();
+    std::vector<sv_frame_t> getDefaultKeyFrames();
+
+    View *m_above;
+    View *m_below;
+};
+
+#endif
--- a/view/LayerGeometryProvider.h	Tue Oct 20 12:55:09 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,130 +0,0 @@
-/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
-
-/*
-    Sonic Visualiser
-    An audio file viewer and annotation editor.
-    Centre for Digital Music, Queen Mary, University of London.
-    
-    This program is free software; you can redistribute it and/or
-    modify it under the terms of the GNU General Public License as
-    published by the Free Software Foundation; either version 2 of the
-    License, or (at your option) any later version.  See the file
-    COPYING included with this distribution for more information.
-*/
-
-#ifndef LAYER_GEOMETRY_PROVIDER_H
-#define LAYER_GEOMETRY_PROVIDER_H
-
-#include "base/BaseTypes.h"
-
-class ViewManager;
-class View;
-class Layer;
-
-class LayerGeometryProvider
-{
-public:
-    /**
-     * Retrieve the first visible sample frame on the widget.
-     * This is a calculated value based on the centre-frame, widget
-     * width and zoom level.  The result may be negative.
-     */
-    virtual sv_frame_t getStartFrame() const = 0;
-
-    /**
-     * Return the centre frame of the visible widget.  This is an
-     * exact value that does not depend on the zoom block size.  Other
-     * frame values (start, end) are calculated from this based on the
-     * zoom and other factors.
-     */
-    virtual sv_frame_t getCentreFrame() const = 0;
-
-    /**
-     * Retrieve the last visible sample frame on the widget.
-     * This is a calculated value based on the centre-frame, widget
-     * width and zoom level.
-     */
-    virtual sv_frame_t getEndFrame() const = 0;
-
-    /**
-     * Return the pixel x-coordinate corresponding to a given sample
-     * frame (which may be negative).
-     */
-    virtual int getXForFrame(sv_frame_t frame) const = 0;
-
-    /**
-     * Return the closest frame to the given pixel x-coordinate.
-     */
-    virtual sv_frame_t getFrameForX(int x) const = 0;
-
-    virtual sv_frame_t getModelsStartFrame() const = 0;
-    virtual sv_frame_t getModelsEndFrame() const = 0;
-
-    /**
-     * Return the pixel y-coordinate corresponding to a given
-     * frequency, if the frequency range is as specified.  This does
-     * not imply any policy about layer frequency ranges, but it might
-     * be useful for layers to match theirs up if desired.
-     *
-     * Not thread-safe in logarithmic mode.  Call only from GUI thread.
-     */
-    virtual double getYForFrequency(double frequency, double minFreq, double maxFreq, 
-                                    bool logarithmic) const = 0;
-
-    /**
-     * Return the closest frequency to the given pixel y-coordinate,
-     * if the frequency range is as specified.
-     *
-     * Not thread-safe in logarithmic mode.  Call only from GUI thread.
-     */
-    virtual double getFrequencyForY(int y, double minFreq, double maxFreq,
-			   bool logarithmic) const = 0;
-
-    virtual int getTextLabelHeight(const Layer *layer, QPainter &) const = 0;
-
-    virtual bool getValueExtents(QString unit, double &min, double &max,
-                                 bool &log) const = 0;
-
-    /**
-     * Return the zoom level, i.e. the number of frames per pixel
-     */
-    virtual int getZoomLevel() const = 0;
-
-    /**
-     * To be called from a layer, to obtain the extent of the surface
-     * that the layer is currently painting to. This may be the extent
-     * of the view (if 1x display scaling is in effect) or of a larger
-     * cached pixmap (if greater display scaling is in effect).
-     */
-    virtual QRect getPaintRect() const = 0;
-
-    virtual QSize getPaintSize() const { return getPaintRect().size(); }
-    virtual int getPaintWidth() const { return getPaintRect().width(); }
-    virtual int getPaintHeight() const { return getPaintRect().height(); }
-
-    virtual bool hasLightBackground() const = 0;
-    virtual QColor getForeground() const = 0;
-    virtual QColor getBackground() const = 0;
-
-    virtual ViewManager *getViewManager() const = 0;
-
-    virtual bool shouldIlluminateLocalFeatures(const Layer *, QPoint &) const = 0;
-    virtual bool shouldShowFeatureLabels() const = 0;
-
-    enum TextStyle {
-	BoxedText,
-	OutlinedText,
-        OutlinedItalicText
-    };
-
-    virtual void drawVisibleText(QPainter &p, int x, int y,
-				 QString text, TextStyle style) const = 0;
-
-    virtual void drawMeasurementRect(QPainter &p, const Layer *,
-                                     QRect rect, bool focus) const = 0;
-
-    virtual View *getView() = 0;
-    virtual const View *getView() const = 0;
-};
-
-#endif
--- a/view/Pane.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/view/Pane.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -25,6 +25,7 @@
 #include "base/Preferences.h"
 #include "layer/WaveformLayer.h"
 #include "layer/TimeRulerLayer.h"
+#include "layer/PaintAssistant.h"
 
 // GF: added so we can propagate the mouse move event to the note layer for context handling.
 #include "layer/LayerFactory.h"
@@ -717,6 +718,10 @@
 void
 Pane::drawCentreLine(sv_samplerate_t sampleRate, QPainter &paint, bool omitLine)
 {
+    if (omitLine && m_manager->getMainModelSampleRate() == 0) {
+        return;
+    }
+    
     int fontHeight = paint.fontMetrics().height();
     int fontAscent = paint.fontMetrics().ascent();
 
@@ -773,14 +778,14 @@
             int tw = paint.fontMetrics().width(text);
             int x = width()/2 - 4 - tw;
             
-            drawVisibleText(paint, x, y, text, OutlinedText);
+            PaintAssistant::drawVisibleText(this, paint, x, y, text, PaintAssistant::OutlinedText);
         }
         
         QString text = QString("%1").arg(m_centreFrame);
         
         int x = width()/2 + 4;
         
-        drawVisibleText(paint, x, y, text, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, x, y, text, PaintAssistant::OutlinedText);
     }
 }
 
@@ -862,8 +867,8 @@
         return;
     }
     
-    drawVisibleText(paint, m_scaleWidth + 5,
-                    paint.fontMetrics().ascent() + y, text, OutlinedText);
+    PaintAssistant::drawVisibleText(this, paint, m_scaleWidth + 5,
+                    paint.fontMetrics().ascent() + y, text, PaintAssistant::OutlinedText);
 
     paint.restore();
 }
@@ -901,8 +906,8 @@
         return;
     }
     
-    drawVisibleText(paint, m_scaleWidth + 5,
-                    paint.fontMetrics().ascent() + y, text, OutlinedText);
+    PaintAssistant::drawVisibleText(this, paint, m_scaleWidth + 5,
+                    paint.fontMetrics().ascent() + y, text, PaintAssistant::OutlinedText);
 
     paint.restore();
 }
@@ -950,9 +955,9 @@
                 paint.setPen(getForeground());
             }
             
-            drawVisibleText(paint, llx,
+            PaintAssistant::drawVisibleText(this, paint, llx,
                             lly - fontHeight + fontAscent,
-                            texts[i], OutlinedText);
+                            texts[i], PaintAssistant::OutlinedText);
 
             if (!pixmaps[i].isNull()) {
                 paint.drawPixmap(llx - fontAscent - 3,
@@ -1014,10 +1019,10 @@
             offsetText = tr("+%1").arg(offsetText);
         }
     }
-    drawVisibleText(paint, p0 + 2, fontAscent + fontHeight + 4, startText, OutlinedText);
-    drawVisibleText(paint, p1 + 2, fontAscent + fontHeight + 4, endText, OutlinedText);
-    drawVisibleText(paint, p0 + 2, fontAscent + fontHeight*2 + 4, offsetText, OutlinedText);
-    drawVisibleText(paint, p1 + 2, fontAscent + fontHeight*2 + 4, offsetText, OutlinedText);
+    PaintAssistant::drawVisibleText(this, paint, p0 + 2, fontAscent + fontHeight + 4, startText, PaintAssistant::OutlinedText);
+    PaintAssistant::drawVisibleText(this, paint, p1 + 2, fontAscent + fontHeight + 4, endText, PaintAssistant::OutlinedText);
+    PaintAssistant::drawVisibleText(this, paint, p0 + 2, fontAscent + fontHeight*2 + 4, offsetText, PaintAssistant::OutlinedText);
+    PaintAssistant::drawVisibleText(this, paint, p1 + 2, fontAscent + fontHeight*2 + 4, offsetText, PaintAssistant::OutlinedText);
     
     //!!! duplicating display policy with View::drawSelections
     
@@ -1080,9 +1085,9 @@
     if (x < pbw + 5) x = pbw + 5;
 
     if (r.x() < x + paint.fontMetrics().width(desc)) {
-        drawVisibleText(paint, x,
+        PaintAssistant::drawVisibleText(this, paint, x,
                         height() - fontHeight + fontAscent - 6,
-                        desc, OutlinedText);
+                        desc, PaintAssistant::OutlinedText);
     }
 }
 
@@ -1943,7 +1948,7 @@
          true, // can move horiz
          canTopLayerMoveVertical(), // can move vert
          canTopLayerMoveVertical() || (m_manager && m_manager->isPlaying()), // resist horiz
-         !(m_manager && m_manager->isPlaying())); // resist vert
+         true); // resist vert
 
     if (m_dragMode == HorizontalDrag ||
         m_dragMode == FreeDrag) {
@@ -2312,7 +2317,7 @@
             m_pendingWheelAngle = 0;
             return;
         }
-        
+
         while (abs(m_pendingWheelAngle) >= 120) {
 
             int sign = (m_pendingWheelAngle < 0 ? -1 : 1);
--- a/view/PaneStack.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/view/PaneStack.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -21,6 +21,7 @@
 #include "widgets/ClickableLabel.h"
 #include "layer/Layer.h"
 #include "ViewManager.h"
+#include "AlignmentView.h"
 
 #include <QApplication>
 #include <QHBoxLayout>
@@ -40,6 +41,7 @@
     QFrame(parent),
     m_currentPane(0),
     m_showAccessories(true),
+    m_showAlignmentViews(false),
     m_splitter(new QSplitter),
     m_propertyStackStack(new QStackedWidget),
     m_viewManager(viewManager),
@@ -67,6 +69,15 @@
     m_showAccessories = show;
 }
 
+void
+PaneStack::setShowAlignmentViews(bool show)
+{
+    m_showAlignmentViews = show;
+    foreach (const PaneRec &r, m_panes) {
+        r.alignmentView->setVisible(m_showAlignmentViews);
+    }
+}
+
 Pane *
 PaneStack::addPane(bool suppressPropertyBox)
 {
@@ -112,6 +123,12 @@
     layout->addWidget(pane, 0, 1, 2, 1);
     layout->setColumnStretch(1, 20);
 
+    AlignmentView *av = new AlignmentView(frame);
+    av->setFixedHeight(40);//!!!
+    av->setVisible(m_showAlignmentViews);
+    av->setViewManager(m_viewManager);
+    layout->addWidget(av, 2, 1);
+
     QWidget *properties = 0;
     if (suppressPropertyBox) {
 	properties = new QFrame();
@@ -139,6 +156,7 @@
     rec.currentIndicator = currentIndicator;
     rec.frame = frame;
     rec.layout = layout;
+    rec.alignmentView = av;
     m_panes.push_back(rec);
 
     frame->setLayout(layout);
@@ -167,11 +185,34 @@
     }
 
     showOrHidePaneAccessories();
+    relinkAlignmentViews();
 
     return pane;
 }
 
 void
+PaneStack::relinkAlignmentViews()
+{
+    for (int i = 0; i < (int)m_panes.size(); ++i) {
+        m_panes[i].alignmentView->setViewAbove(m_panes[i].pane);
+        if (i + 1 < (int)m_panes.size()) {
+            m_panes[i].alignmentView->setViewBelow(m_panes[i+1].pane);
+        } else {
+            m_panes[i].alignmentView->setViewBelow(0);
+        }
+    }
+}
+
+void
+PaneStack::unlinkAlignmentViews()
+{
+    for (int i = 0; i < (int)m_panes.size(); ++i) {
+        m_panes[i].alignmentView->setViewAbove(0);
+        m_panes[i].alignmentView->setViewBelow(0);
+    }
+}
+
+void
 PaneStack::setPropertyStackMinWidth(int mw)
 {
     for (std::vector<PaneRec>::iterator i = m_panes.begin();
@@ -279,6 +320,7 @@
     }
 
     emit paneAboutToBeDeleted(pane);
+    unlinkAlignmentViews();
 
     cerr << "PaneStack::deletePane: about to delete parent " << pane->parent() << " of pane " << pane << endl;
 
@@ -303,6 +345,7 @@
     }
 
     showOrHidePaneAccessories();
+    relinkAlignmentViews();
 
     emit paneDeleted();
 }
@@ -362,6 +405,8 @@
 	++i;
     }
 
+    relinkAlignmentViews();
+
     cerr << "WARNING: PaneStack::hidePane(" << pane << "): Pane not found in visible panes" << endl;
 }
 
@@ -386,6 +431,8 @@
 	++i;
     }
 
+    relinkAlignmentViews();
+
     cerr << "WARNING: PaneStack::showPane(" << pane << "): Pane not found in hidden panes" << endl;
 }
 
--- a/view/PaneStack.h	Tue Oct 20 12:55:09 2015 +0100
+++ b/view/PaneStack.h	Fri Aug 19 15:58:57 2016 +0100
@@ -34,6 +34,7 @@
 class ViewManager;
 class PropertyContainer;
 class PropertyStack;
+class AlignmentView;
 
 class PaneStack : public QFrame
 {
@@ -73,6 +74,8 @@
     
     void setShowPaneAccessories(bool show); // current indicator, close button
 
+    void setShowAlignmentViews(bool show);
+
     void sizePanesEqually();
 
 signals:
@@ -114,18 +117,20 @@
 
     struct PaneRec
     {
-	Pane        *pane;
-	QWidget     *propertyStack;
-        QPushButton *xButton;
-	QLabel      *currentIndicator;
-        QFrame      *frame;
-        QGridLayout *layout;
+	Pane          *pane;
+	QWidget       *propertyStack;
+        QPushButton   *xButton;
+	QLabel        *currentIndicator;
+        QFrame        *frame;
+        QGridLayout   *layout;
+        AlignmentView *alignmentView;
     };
 
     std::vector<PaneRec> m_panes;
     std::vector<PaneRec> m_hiddenPanes;
 
     bool m_showAccessories;
+    bool m_showAlignmentViews;
 
     QSplitter *m_splitter;
     QStackedWidget *m_propertyStackStack;
@@ -136,6 +141,9 @@
 
     void showOrHidePaneAccessories();
 
+    void unlinkAlignmentViews();
+    void relinkAlignmentViews();
+
     LayoutStyle m_layoutStyle;
 };
 
--- a/view/View.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/view/View.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -24,6 +24,8 @@
 
 #include "layer/TimeRulerLayer.h"
 #include "layer/SingleColourLayer.h"
+#include "layer/PaintAssistant.h"
+
 #include "data/model/PowerOfSqrtTwoZoomConstraint.h"
 #include "data/model/RangeSummarisableTimeValueModel.h"
 
@@ -49,9 +51,9 @@
 //#define DEBUG_VIEW 1
 //#define DEBUG_VIEW_WIDGET_PAINT 1
 
-
 View::View(QWidget *w, bool showProgress) :
     QFrame(w),
+    m_id(getNextId()),
     m_centreFrame(0),
     m_zoomLevel(1024),
     m_followPan(true),
@@ -365,15 +367,17 @@
 sv_frame_t
 View::getFrameForX(int x) const
 {
-    int z = m_zoomLevel;
+    sv_frame_t z = m_zoomLevel; // nb not just int, or multiplication may overflow
     sv_frame_t frame = m_centreFrame - (width()/2) * z;
 
+    frame = (frame / z) * z; // this is start frame
+    frame = frame + x * z;
+
 #ifdef DEBUG_VIEW_WIDGET_PAINT
-    SVDEBUG << "View::getFrameForX(" << x << "): z = " << z << ", m_centreFrame = " << m_centreFrame << ", width() = " << width() << ", frame = " << frame << endl;
+    cerr << "View::getFrameForX(" << x << "): z = " << z << ", m_centreFrame = " << m_centreFrame << ", width() = " << width() << ", frame = " << frame << endl;
 #endif
 
-    frame = (frame / z) * z; // this is start frame
-    return frame + x * z;
+    return frame;
 }
 
 double
@@ -411,12 +415,12 @@
 }
 
 double
-View::getFrequencyForY(int y,
+View::getFrequencyForY(double y,
 		       double minf,
 		       double maxf,
 		       bool logarithmic) const
 {
-    int h = height();
+    double h = height();
 
     if (logarithmic) {
 
@@ -808,56 +812,6 @@
 }
 
 void
-View::drawVisibleText(QPainter &paint, int x, int y, QString text, TextStyle style) const
-{
-    if (style == OutlinedText || style == OutlinedItalicText) {
-
-        paint.save();
-
-        if (style == OutlinedItalicText) {
-            QFont f(paint.font());
-            f.setItalic(true);
-            paint.setFont(f);
-        }
-
-        QColor penColour, surroundColour, boxColour;
-
-        penColour = getForeground();
-        surroundColour = getBackground();
-        boxColour = surroundColour;
-        boxColour.setAlpha(127);
-
-        paint.setPen(Qt::NoPen);
-        paint.setBrush(boxColour);
-        
-        QRect r = paint.fontMetrics().boundingRect(text);
-        r.translate(QPoint(x, y));
-//        cerr << "drawVisibleText: r = " << r.x() << "," <<r.y() << " " << r.width() << "x" << r.height() << endl;
-        paint.drawRect(r);
-        paint.setBrush(Qt::NoBrush);
-
-	paint.setPen(surroundColour);
-
-	for (int dx = -1; dx <= 1; ++dx) {
-	    for (int dy = -1; dy <= 1; ++dy) {
-		if (!(dx || dy)) continue;
-		paint.drawText(x + dx, y + dy, text);
-	    }
-	}
-
-	paint.setPen(penColour);
-
-	paint.drawText(x, y, text);
-
-        paint.restore();
-
-    } else {
-
-	cerr << "ERROR: View::drawVisibleText: Boxed style not yet implemented!" << endl;
-    }
-}
-
-void
 View::setPlaybackFollow(PlaybackFollowMode m)
 {
     m_followPlay = m;
@@ -2367,32 +2321,32 @@
     }
     
     if (axs != "") {
-        drawVisibleText(paint, axx, axy, axs, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, axx, axy, axs, PaintAssistant::OutlinedText);
         axy += fontHeight;
     }
     
     if (ays != "") {
-        drawVisibleText(paint, axx, axy, ays, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, axx, axy, ays, PaintAssistant::OutlinedText);
         axy += fontHeight;
     }
 
     if (bxs != "") {
-        drawVisibleText(paint, bxx, bxy, bxs, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, bxx, bxy, bxs, PaintAssistant::OutlinedText);
         bxy += fontHeight;
     }
 
     if (bys != "") {
-        drawVisibleText(paint, bxx, bxy, bys, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, bxx, bxy, bys, PaintAssistant::OutlinedText);
         bxy += fontHeight;
     }
 
     if (dxs != "") {
-        drawVisibleText(paint, dxx, dxy, dxs, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, dxx, dxy, dxs, PaintAssistant::OutlinedText);
         dxy += fontHeight;
     }
 
     if (dys != "") {
-        drawVisibleText(paint, dxx, dxy, dys, OutlinedText);
+        PaintAssistant::drawVisibleText(this, paint, dxx, dxy, dys, PaintAssistant::OutlinedText);
         dxy += fontHeight;
     }
 
--- a/view/View.h	Tue Oct 20 12:55:09 2015 +0100
+++ b/view/View.h	Fri Aug 19 15:58:57 2016 +0100
@@ -19,7 +19,7 @@
 #include <QFrame>
 #include <QProgressBar>
 
-#include "LayerGeometryProvider.h"
+#include "layer/LayerGeometryProvider.h"
 
 #include "base/ZoomConstraint.h"
 #include "base/PropertyContainer.h"
@@ -64,6 +64,12 @@
     virtual ~View();
 
     /**
+     * Retrieve the id of this object. Views have their own unique
+     * ids, but ViewProxy objects share the id of their View.
+     */
+    int getId() const { return m_id; }
+    
+    /**
      * Retrieve the first visible sample frame on the widget.
      * This is a calculated value based on the centre-frame, widget
      * width and zoom level.  The result may be negative.
@@ -108,6 +114,20 @@
     sv_frame_t getFrameForX(int x) const;
 
     /**
+     * Return the closest pixel x-coordinate corresponding to a given
+     * view x-coordinate. Default is no scaling, ViewProxy handles
+     * scaling case.
+     */
+    int getXForViewX(int viewx) const { return viewx; }
+
+    /**
+     * Return the closest view x-coordinate corresponding to a given
+     * pixel x-coordinate. Default is no scaling, ViewProxy handles
+     * scaling case.
+     */
+    int getViewXForX(int x) const { return x; }
+
+    /**
      * Return the pixel y-coordinate corresponding to a given
      * frequency, if the frequency range is as specified.  This does
      * not imply any policy about layer frequency ranges, but it might
@@ -124,8 +144,8 @@
      *
      * Not thread-safe in logarithmic mode.  Call only from GUI thread.
      */
-    double getFrequencyForY(int y, double minFreq, double maxFreq,
-			   bool logarithmic) const;
+    double getFrequencyForY(double y, double minFreq, double maxFreq,
+                            bool logarithmic) const;
 
     /**
      * Return the zoom level, i.e. the number of frames per pixel
@@ -246,9 +266,6 @@
     virtual QColor getForeground() const;
     virtual QColor getBackground() const;
 
-    virtual void drawVisibleText(QPainter &p, int x, int y,
-				 QString text, TextStyle style) const;
-
     virtual void drawMeasurementRect(QPainter &p, const Layer *,
                                      QRect rect, bool focus) const;
 
@@ -333,6 +350,8 @@
     sv_frame_t alignToReference(sv_frame_t) const;
     sv_frame_t getAlignedPlaybackFrame() const;
 
+    void updatePaintRect(QRect r) { update(r); }
+    
     View *getView() { return this; } 
     const View *getView() const { return this; } 
     
@@ -351,7 +370,7 @@
                             bool globalScroll,
                             PlaybackFollowMode followMode);
 
-    void zoomLevelChanged(int, bool);
+    void zoomLevelChanged(int level, bool locked);
 
     void contextHelpChanged(const QString &);
 
@@ -384,6 +403,9 @@
 
 protected:
     View(QWidget *, bool showProgress);
+
+    int m_id;
+    
     virtual void paintEvent(QPaintEvent *e);
     virtual void drawSelections(QPainter &);
     virtual bool shouldLabelSelections() const { return true; }
--- a/view/ViewManager.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/view/ViewManager.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -741,3 +741,4 @@
     if (pixels != 0 && scaled == 0) scaled = 1;
     return scaled;
 }
+
--- a/view/ViewProxy.h	Tue Oct 20 12:55:09 2015 +0100
+++ b/view/ViewProxy.h	Fri Aug 19 15:58:57 2016 +0100
@@ -15,7 +15,7 @@
 #ifndef VIEW_PROXY_H
 #define VIEW_PROXY_H
 
-#include "LayerGeometryProvider.h"
+#include "layer/LayerGeometryProvider.h"
 
 class ViewProxy : public LayerGeometryProvider
 {
@@ -23,6 +23,9 @@
     ViewProxy(View *view, int scaleFactor) :
 	m_view(view), m_scaleFactor(scaleFactor) { }
 
+    virtual int getId() const {
+        return m_view->getId();
+    }
     virtual sv_frame_t getStartFrame() const {
 	return m_view->getStartFrame();
     }
@@ -42,6 +45,12 @@
         sv_frame_t f1 = m_view->getFrameForX((x / m_scaleFactor) + 1);
         return f0 + ((f1 - f0) * (x % m_scaleFactor)) / m_scaleFactor;
     }
+    virtual int getXForViewX(int viewx) const {
+        return viewx * m_scaleFactor;
+    }
+    virtual int getViewXForX(int x) const {
+        return x / m_scaleFactor;
+    }
     virtual sv_frame_t getModelsStartFrame() const {
 	return m_view->getModelsStartFrame();
     }
@@ -54,14 +63,10 @@
 	return m_scaleFactor *
 	    m_view->getYForFrequency(frequency, minFreq, maxFreq, logarithmic);
     }
-    virtual double getFrequencyForY(int y, double minFreq, double maxFreq,
+    virtual double getFrequencyForY(double y, double minFreq, double maxFreq,
 				    bool logarithmic) const {
-        double f0 = m_view->getFrequencyForY
+        return m_view->getFrequencyForY
             (y / m_scaleFactor, minFreq, maxFreq, logarithmic);
-        if (m_scaleFactor == 1) return f0;
-        double f1 = m_view->getFrequencyForY
-            ((y / m_scaleFactor) + 1, minFreq, maxFreq, logarithmic);
-        return f0 + ((f1 - f0) * (y % m_scaleFactor)) / m_scaleFactor;
     }
     virtual int getTextLabelHeight(const Layer *layer, QPainter &paint) const {
 	return m_scaleFactor * m_view->getTextLabelHeight(layer, paint);
@@ -119,16 +124,18 @@
 	return m_view->shouldShowFeatureLabels();
     }
 
-    virtual void drawVisibleText(QPainter &p, int x, int y,
-				 QString text, TextStyle style) const {
-	m_view->drawVisibleText(p, x, y, text, style);
-    }
-
     virtual void drawMeasurementRect(QPainter &p, const Layer *layer,
                                      QRect rect, bool focus) const {
 	m_view->drawMeasurementRect(p, layer, rect, focus);
     }
 
+    virtual void updatePaintRect(QRect r) {
+        m_view->update(r.x() / m_scaleFactor,
+                       r.y() / m_scaleFactor,
+                       r.width() / m_scaleFactor,
+                       r.height() / m_scaleFactor);
+    }
+    
     virtual View *getView() { return m_view; }
     virtual const View *getView() const { return m_view; }
 
--- a/widgets/AudioDial.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/widgets/AudioDial.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -409,13 +409,27 @@
 
     if (m_showTooltip) {
         QString name = objectName();
-        QString unit = "";
+        QString label;
+        if (m_rangeMapper) {
+            label = m_rangeMapper->getLabel(value);
+        }
         QString text;
-        if (m_rangeMapper) unit = m_rangeMapper->getUnit();
-        if (name != "") {
-            text = tr("%1: %2%3").arg(name).arg(m_mappedValue).arg(unit);
+        if (label != "") {
+            if (name != "") {
+                text = tr("%1: %2").arg(name).arg(label);
+            } else {
+                text = label;
+            }
         } else {
-            text = tr("%2%3").arg(m_mappedValue).arg(unit);
+            QString unit = "";
+            if (m_rangeMapper) {
+                unit = m_rangeMapper->getUnit();
+            }
+            if (name != "") {
+                text = tr("%1: %2%3").arg(name).arg(m_mappedValue).arg(unit);
+            } else {
+                text = tr("%2%3").arg(m_mappedValue).arg(unit);
+            }
         }
         setToolTip(text);
     }
--- a/widgets/PropertyBox.cpp	Tue Oct 20 12:55:09 2015 +0100
+++ b/widgets/PropertyBox.cpp	Fri Aug 19 15:58:57 2016 +0100
@@ -212,10 +212,10 @@
 	gainDial->setFixedWidth(24);
 	gainDial->setFixedHeight(24);
 	gainDial->setNotchesVisible(false);
-	gainDial->setDefaultValue(0);
         gainDial->setObjectName(tr("Playback Gain"));
         gainDial->setRangeMapper(new LinearRangeMapper
                                  (-50, 50, -25, 25, tr("dB")));
+	gainDial->setDefaultValue(0);
         gainDial->setShowToolTip(true);
 	connect(gainDial, SIGNAL(valueChanged(int)),
 		this, SLOT(playGainDialChanged(int)));
@@ -384,8 +384,10 @@
 	    dial->setMaximum(max);
 	    dial->setPageStep(1);
 	    dial->setNotchesVisible((max - min) <= 12);
+            // important to set the range mapper before the default,
+            // because the range mapper is used to map the default
+            dial->setRangeMapper(m_container->getNewPropertyRangeMapper(name));
 	    dial->setDefaultValue(deflt);
-            dial->setRangeMapper(m_container->getNewPropertyRangeMapper(name));
             dial->setShowToolTip(true);
 	    connect(dial, SIGNAL(valueChanged(int)),
 		    this, SLOT(propertyControllerChanged(int)));