changeset 891:8962f80f5d8e tony_integration

Merge from default branch
author Chris Cannam
date Tue, 11 Mar 2014 17:32:31 +0000
parents 4cbf8c6a462d (current diff) 7f97a4d9d14f (diff)
children 1341cc1390be
files transform/FeatureExtractionModelTransformer.cpp
diffstat 11 files changed, 910 insertions(+), 32 deletions(-) [+]
line wrap: on
line diff
--- a/base/RangeMapper.cpp	Tue Mar 11 17:30:35 2014 +0000
+++ b/base/RangeMapper.cpp	Tue Mar 11 17:32:31 2014 +0000
@@ -38,28 +38,38 @@
 int
 LinearRangeMapper::getPositionForValue(float value) const
 {
+    int position = getPositionForValueUnclamped(value);
+    if (position < m_minpos) position = m_minpos;
+    if (position > m_maxpos) position = m_maxpos;
+    return position;
+}
+
+int
+LinearRangeMapper::getPositionForValueUnclamped(float value) const
+{
     int position = m_minpos +
         lrintf(((value - m_minval) / (m_maxval - m_minval))
                * (m_maxpos - m_minpos));
-    if (position < m_minpos) position = m_minpos;
-    if (position > m_maxpos) position = m_maxpos;
-//    SVDEBUG << "LinearRangeMapper::getPositionForValue: " << value << " -> "
-//              << position << " (minpos " << m_minpos << ", maxpos " << m_maxpos << ", minval " << m_minval << ", maxval " << m_maxval << ")" << endl;
-    if (m_inverted) return m_maxpos - position;
+    if (m_inverted) return m_maxpos - (position - m_minpos);
     else return position;
 }
 
 float
 LinearRangeMapper::getValueForPosition(int position) const
 {
-    if (m_inverted) position = m_maxpos - position;
+    if (position < m_minpos) position = m_minpos;
+    if (position > m_maxpos) position = m_maxpos;
+    float value = getValueForPositionUnclamped(position);
+    return value;
+}
+
+float
+LinearRangeMapper::getValueForPositionUnclamped(int position) const
+{
+    if (m_inverted) position = m_maxpos - (position - m_minpos);
     float value = m_minval +
         ((float(position - m_minpos) / float(m_maxpos - m_minpos))
          * (m_maxval - m_minval));
-    if (value < m_minval) value = m_minval;
-    if (value > m_maxval) value = m_maxval;
-//    SVDEBUG << "LinearRangeMapper::getValueForPosition: " << position << " -> "
-//              << value << " (minpos " << m_minpos << ", maxpos " << m_maxpos << ", minval " << m_minval << ", maxval " << m_maxval << ")" << endl;
     return value;
 }
 
@@ -73,14 +83,16 @@
 {
     convertMinMax(minpos, maxpos, minval, maxval, m_minlog, m_ratio);
 
-    cerr << "LogRangeMapper: minpos " << minpos << ", maxpos "
-              << maxpos << ", minval " << minval << ", maxval "
-              << maxval << ", minlog " << m_minlog << ", ratio " << m_ratio
-              << ", unit " << unit << endl;
+//    cerr << "LogRangeMapper: minpos " << minpos << ", maxpos "
+//              << maxpos << ", minval " << minval << ", maxval "
+//              << maxval << ", minlog " << m_minlog << ", ratio " << m_ratio
+//              << ", unit " << unit << endl;
 
     assert(m_maxpos != m_minpos);
 
     m_maxlog = (m_maxpos - m_minpos) / m_ratio + m_minlog;
+
+//    cerr << "LogRangeMapper: maxlog = " << m_maxlog << endl;
 }
 
 void
@@ -106,22 +118,223 @@
 int
 LogRangeMapper::getPositionForValue(float value) const
 {
-    int position = (log10(value) - m_minlog) * m_ratio + m_minpos;
+    int position = getPositionForValueUnclamped(value);
     if (position < m_minpos) position = m_minpos;
     if (position > m_maxpos) position = m_maxpos;
-//    SVDEBUG << "LogRangeMapper::getPositionForValue: " << value << " -> "
-//              << position << " (minpos " << m_minpos << ", maxpos " << m_maxpos << ", ratio " << m_ratio << ", minlog " << m_minlog << ")" << endl;
-    if (m_inverted) return m_maxpos - position;
+    return position;
+}
+
+int
+LogRangeMapper::getPositionForValueUnclamped(float value) const
+{
+    static float thresh = powf(10, -10);
+    if (value < thresh) value = thresh;
+    int position = lrintf((log10(value) - m_minlog) * m_ratio) + m_minpos;
+    if (m_inverted) return m_maxpos - (position - m_minpos);
     else return position;
 }
 
 float
 LogRangeMapper::getValueForPosition(int position) const
 {
-    if (m_inverted) position = m_maxpos - position;
-    float value = powf(10, (position - m_minpos) / m_ratio + m_minlog);
-//    SVDEBUG << "LogRangeMapper::getValueForPosition: " << position << " -> "
-//              << value << " (minpos " << m_minpos << ", maxpos " << m_maxpos << ", ratio " << m_ratio << ", minlog " << m_minlog << ")" << endl;
+    if (position < m_minpos) position = m_minpos;
+    if (position > m_maxpos) position = m_maxpos;
+    float value = getValueForPositionUnclamped(position);
     return value;
 }
 
+float
+LogRangeMapper::getValueForPositionUnclamped(int position) const
+{
+    if (m_inverted) position = m_maxpos - (position - m_minpos);
+    float value = powf(10, (position - m_minpos) / m_ratio + m_minlog);
+    return value;
+}
+
+InterpolatingRangeMapper::InterpolatingRangeMapper(CoordMap pointMappings,
+                                                   QString unit) :
+    m_mappings(pointMappings),
+    m_unit(unit)
+{
+    for (CoordMap::const_iterator i = m_mappings.begin(); 
+         i != m_mappings.end(); ++i) {
+        m_reverse[i->second] = i->first;
+    }
+}
+
+int
+InterpolatingRangeMapper::getPositionForValue(float value) const
+{
+    int pos = getPositionForValueUnclamped(value);
+    CoordMap::const_iterator i = m_mappings.begin();
+    if (pos < i->second) pos = i->second;
+    i = m_mappings.end(); --i;
+    if (pos > i->second) pos = i->second;
+    return pos;
+}
+
+int
+InterpolatingRangeMapper::getPositionForValueUnclamped(float value) const
+{
+    float p = interpolate(&m_mappings, value);
+    return lrintf(p);
+}
+
+float
+InterpolatingRangeMapper::getValueForPosition(int position) const
+{
+    float val = getValueForPositionUnclamped(position);
+    CoordMap::const_iterator i = m_mappings.begin();
+    if (val < i->first) val = i->first;
+    i = m_mappings.end(); --i;
+    if (val > i->first) val = i->first;
+    return val;
+}
+
+float
+InterpolatingRangeMapper::getValueForPositionUnclamped(int position) const
+{
+    return interpolate(&m_reverse, position);
+}
+
+template <typename T>
+float
+InterpolatingRangeMapper::interpolate(T *mapping, float value) const
+{
+    // lower_bound: first element which does not compare less than value
+    typename T::const_iterator i = mapping->lower_bound(value);
+
+    if (i == mapping->begin()) {
+        // value is less than or equal to first element, so use the
+        // gradient from first to second and extend it
+        ++i;
+    }
+
+    if (i == mapping->end()) {
+        // value is off the end, so use the gradient from penultimate
+        // to ultimate and extend it
+        --i;
+    }
+
+    typename T::const_iterator j = i;
+    --j;
+
+    float gradient = float(i->second - j->second) / float(i->first - j->first);
+
+    return j->second + (value - j->first) * gradient;
+}
+
+AutoRangeMapper::AutoRangeMapper(CoordMap pointMappings,
+                                 QString unit) :
+    m_mappings(pointMappings),
+    m_unit(unit)
+{
+    m_type = chooseMappingTypeFor(m_mappings);
+
+    CoordMap::const_iterator first = m_mappings.begin();
+    CoordMap::const_iterator last = m_mappings.end();
+    --last;
+
+    switch (m_type) {
+    case StraightLine:
+        m_mapper = new LinearRangeMapper(first->second, last->second,
+                                         first->first, last->first,
+                                         unit, false);
+        break;
+    case Logarithmic:
+        m_mapper = new LogRangeMapper(first->second, last->second,
+                                      first->first, last->first,
+                                      unit, false);
+        break;
+    case Interpolating:
+        m_mapper = new InterpolatingRangeMapper(m_mappings, unit);
+        break;
+    }
+}
+
+AutoRangeMapper::~AutoRangeMapper()
+{
+    delete m_mapper;
+}
+
+AutoRangeMapper::MappingType
+AutoRangeMapper::chooseMappingTypeFor(const CoordMap &mappings)
+{
+    // how do we work out whether a linear/log mapping is "close enough"?
+
+    CoordMap::const_iterator first = mappings.begin();
+    CoordMap::const_iterator last = mappings.end();
+    --last;
+
+    LinearRangeMapper linm(first->second, last->second,
+                           first->first, last->first,
+                           "", false);
+
+    bool inadequate = false;
+
+    for (CoordMap::const_iterator i = mappings.begin();
+         i != mappings.end(); ++i) {
+        int candidate = linm.getPositionForValue(i->first);
+        int diff = candidate - i->second;
+        if (diff < 0) diff = -diff;
+        if (diff > 1) {
+//            cerr << "AutoRangeMapper::chooseMappingTypeFor: diff = " << diff
+//                 << ", straight-line mapping inadequate" << endl;
+            inadequate = true;
+            break;
+        }
+    }
+
+    if (!inadequate) {
+        return StraightLine;
+    }
+
+    LogRangeMapper logm(first->second, last->second,
+                        first->first, last->first,
+                        "", false);
+
+    inadequate = false;
+
+    for (CoordMap::const_iterator i = mappings.begin();
+         i != mappings.end(); ++i) {
+        int candidate = logm.getPositionForValue(i->first);
+        int diff = candidate - i->second;
+        if (diff < 0) diff = -diff;
+        if (diff > 1) {
+//            cerr << "AutoRangeMapper::chooseMappingTypeFor: diff = " << diff
+//                 << ", log mapping inadequate" << endl;
+            inadequate = true;
+            break;
+        }
+    }
+
+    if (!inadequate) {
+        return Logarithmic;
+    }
+
+    return Interpolating;
+}
+
+int
+AutoRangeMapper::getPositionForValue(float value) const
+{
+    return m_mapper->getPositionForValue(value);
+}
+
+float
+AutoRangeMapper::getValueForPosition(int position) const
+{
+    return m_mapper->getValueForPosition(position);
+}
+
+int
+AutoRangeMapper::getPositionForValueUnclamped(float value) const
+{
+    return m_mapper->getPositionForValueUnclamped(value);
+}
+
+float
+AutoRangeMapper::getValueForPositionUnclamped(int position) const
+{
+    return m_mapper->getValueForPositionUnclamped(position);
+}
--- a/base/RangeMapper.h	Tue Mar 11 17:30:35 2014 +0000
+++ b/base/RangeMapper.h	Tue Mar 11 17:32:31 2014 +0000
@@ -19,14 +19,48 @@
 #include <QString>
 
 #include "Debug.h"
-
+#include <map>
 
 class RangeMapper 
 {
 public:
     virtual ~RangeMapper() { }
+
+    /**
+     * Return the position that maps to the given value, rounding to
+     * the nearest position and clamping to the minimum and maximum
+     * extents of the mapper's positional range.
+     */
     virtual int getPositionForValue(float value) const = 0;
+
+    /**
+     * Return the position that maps to the given value, rounding to
+     * the nearest position, without clamping. That is, whatever
+     * mapping function is in use will be projected even outside the
+     * minimum and maximum extents of the mapper's positional
+     * range. (The mapping outside that range is not guaranteed to be
+     * exact, except if the mapper is a linear one.)
+     */
+    virtual int getPositionForValueUnclamped(float value) const = 0;
+
+    /**
+     * Return the value mapped from the given position, clamping to
+     * the minimum and maximum extents of the mapper's value range.
+     */
     virtual float getValueForPosition(int position) const = 0;
+
+    /**
+     * Return the value mapped from the given positionq, without
+     * clamping. That is, whatever mapping function is in use will be
+     * projected even outside the minimum and maximum extents of the
+     * mapper's value range. (The mapping outside that range is not
+     * guaranteed to be exact, except if the mapper is a linear one.)
+     */
+    virtual float getValueForPositionUnclamped(int position) const = 0;
+
+    /**
+     * Get the unit of the mapper's value range.
+     */
     virtual QString getUnit() const { return ""; }
 };
 
@@ -34,12 +68,21 @@
 class LinearRangeMapper : public RangeMapper
 {
 public:
+    /**
+     * Map values in range minval->maxval linearly into integer range
+     * minpos->maxpos. minval and minpos must be less than maxval and
+     * maxpos respectively. If inverted is true, the range will be
+     * mapped "backwards" (minval to maxpos and maxval to minpos).
+     */
     LinearRangeMapper(int minpos, int maxpos,
                       float minval, float maxval,
                       QString unit = "", bool inverted = false);
     
     virtual int getPositionForValue(float value) const;
+    virtual int getPositionForValueUnclamped(float value) const;
+
     virtual float getValueForPosition(int position) const;
+    virtual float getValueForPositionUnclamped(int position) const;
 
     virtual QString getUnit() const { return m_unit; }
 
@@ -52,10 +95,17 @@
     bool m_inverted;
 };
 
-
 class LogRangeMapper : public RangeMapper
 {
 public:
+    /**
+     * Map values in range minval->maxval into integer range
+     * minpos->maxpos such that logs of the values are mapped
+     * linearly. minval must be greater than zero, and minval and
+     * minpos must be less than maxval and maxpos respectively. If
+     * inverted is true, the range will be mapped "backwards" (minval
+     * to maxpos and maxval to minpos).
+     */
     LogRangeMapper(int minpos, int maxpos,
                    float minval, float maxval,
                    QString m_unit = "", bool inverted = false);
@@ -69,7 +119,10 @@
                               float &ratio, float &minlog);
 
     virtual int getPositionForValue(float value) const;
+    virtual int getPositionForValueUnclamped(float value) const;
+
     virtual float getValueForPosition(int position) const;
+    virtual float getValueForPositionUnclamped(int position) const;
 
     virtual QString getUnit() const { return m_unit; }
 
@@ -83,5 +136,120 @@
     bool m_inverted;
 };
 
+class InterpolatingRangeMapper : public RangeMapper
+{
+public:
+    typedef std::map<float, int> CoordMap;
+
+    /**
+     * Given a series of (value, position) coordinate mappings,
+     * construct a range mapper that maps arbitrary values, in the
+     * range between minimum and maximum of the provided values, onto
+     * coordinates using linear interpolation between the supplied
+     * points.
+     *
+     *!!! todo: Cubic -- more generally useful than linear interpolation
+     *!!! todo: inverted flag
+     *
+     * The set of provided mappings must contain at least two
+     * coordinates.
+     *
+     * It is expected that the values and positions in the coordinate
+     * mappings will both be monotonically increasing (i.e. no
+     * inflections in the mapping curve). Behaviour is undefined if
+     * this is not the case.
+     */
+    InterpolatingRangeMapper(CoordMap pointMappings,
+                             QString unit);
+
+    virtual int getPositionForValue(float value) const;
+    virtual int getPositionForValueUnclamped(float value) const;
+
+    virtual float getValueForPosition(int position) const;
+    virtual float getValueForPositionUnclamped(int position) const;
+
+    virtual QString getUnit() const { return m_unit; }
+
+protected:
+    CoordMap m_mappings;
+    std::map<int, float> m_reverse;
+    QString m_unit;
+
+    template <typename T>
+    float interpolate(T *mapping, float v) const;
+};
+
+class AutoRangeMapper : public RangeMapper
+{
+public:
+    enum MappingType {
+        Interpolating,
+        StraightLine,
+        Logarithmic,
+    };
+
+    typedef std::map<float, int> CoordMap;
+
+    /**
+     * Given a series of (value, position) coordinate mappings,
+     * construct a range mapper that maps arbitrary values, in the
+     * range between minimum and maximum of the provided values, onto
+     * coordinates. 
+     *
+     * The mapping used may be
+     * 
+     *    Interpolating -- an InterpolatingRangeMapper will be used
+     * 
+     *    StraightLine -- a LinearRangeMapper from the minimum to
+     *    maximum value coordinates will be used, ignoring all other
+     *    supplied coordinate mappings
+     * 
+     *    Logarithmic -- a LogRangeMapper from the minimum to
+     *    maximum value coordinates will be used, ignoring all other
+     *    supplied coordinate mappings
+     *
+     * The mapping will be chosen automatically by looking at the
+     * supplied coordinates. If the supplied coordinates fall on a
+     * straight line, a StraightLine mapping will be used; if they
+     * fall on a log curve, a Logarithmic mapping will be used;
+     * otherwise an Interpolating mapping will be used.
+     *
+     *!!! todo: inverted flag
+     *
+     * The set of provided mappings must contain at least two
+     * coordinates, or at least three if the points are not supposed
+     * to be in a straight line.
+     *
+     * It is expected that the values and positions in the coordinate
+     * mappings will both be monotonically increasing (i.e. no
+     * inflections in the mapping curve). Behaviour is undefined if
+     * this is not the case.
+     */
+    AutoRangeMapper(CoordMap pointMappings,
+                    QString unit);
+
+    ~AutoRangeMapper();
+
+    /**
+     * Return the mapping type in use.
+     */
+    MappingType getType() const { return m_type; }
+
+    virtual int getPositionForValue(float value) const;
+    virtual int getPositionForValueUnclamped(float value) const;
+
+    virtual float getValueForPosition(int position) const;
+    virtual float getValueForPositionUnclamped(int position) const;
+
+    virtual QString getUnit() const { return m_unit; }
+
+protected:
+    MappingType m_type;
+    CoordMap m_mappings;
+    QString m_unit;
+    RangeMapper *m_mapper;
+
+    MappingType chooseMappingTypeFor(const CoordMap &);
+};
 
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/test/TestRangeMapper.h	Tue Mar 11 17:32:31 2014 +0000
@@ -0,0 +1,284 @@
+/* -*- 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 TEST_RANGE_MAPPER_H
+#define TEST_RANGE_MAPPER_H
+
+#include "../RangeMapper.h"
+
+#include <QObject>
+#include <QtTest>
+#include <QDir>
+
+#include <iostream>
+
+using namespace std;
+
+class TestRangeMapper : public QObject
+{
+    Q_OBJECT
+
+private slots:
+    void linearUpForward()
+    {
+	LinearRangeMapper rm(1, 8, 0.5, 4.0, "x", false);
+	QCOMPARE(rm.getUnit(), QString("x"));
+	QCOMPARE(rm.getPositionForValue(0.5), 1);
+	QCOMPARE(rm.getPositionForValue(4.0), 8);
+	QCOMPARE(rm.getPositionForValue(3.0), 6);
+	QCOMPARE(rm.getPositionForValue(3.1), 6);
+	QCOMPARE(rm.getPositionForValue(3.4), 7);
+	QCOMPARE(rm.getPositionForValue(0.2), 1);
+	QCOMPARE(rm.getPositionForValue(-12), 1);
+	QCOMPARE(rm.getPositionForValue(6.1), 8);
+	QCOMPARE(rm.getPositionForValueUnclamped(3.0), 6);
+	QCOMPARE(rm.getPositionForValueUnclamped(0.2), 0);
+	QCOMPARE(rm.getPositionForValueUnclamped(-12), -24);
+	QCOMPARE(rm.getPositionForValueUnclamped(6.1), 12);
+    }
+
+    void linearDownForward()
+    {
+	LinearRangeMapper rm(1, 8, 0.5, 4.0, "x", true);
+	QCOMPARE(rm.getUnit(), QString("x"));
+	QCOMPARE(rm.getPositionForValue(0.5), 8);
+	QCOMPARE(rm.getPositionForValue(4.0), 1);
+	QCOMPARE(rm.getPositionForValue(3.0), 3);
+	QCOMPARE(rm.getPositionForValue(3.1), 3);
+	QCOMPARE(rm.getPositionForValue(3.4), 2);
+	QCOMPARE(rm.getPositionForValue(0.2), 8);
+	QCOMPARE(rm.getPositionForValue(-12), 8);
+	QCOMPARE(rm.getPositionForValue(6.1), 1);
+	QCOMPARE(rm.getPositionForValueUnclamped(3.0), 3);
+	QCOMPARE(rm.getPositionForValueUnclamped(0.2), 9);
+	QCOMPARE(rm.getPositionForValueUnclamped(-12), 33);
+	QCOMPARE(rm.getPositionForValueUnclamped(6.1), -3);
+    }
+
+    void linearUpBackward()
+    {
+	LinearRangeMapper rm(1, 8, 0.5, 4.0, "x", false);
+	QCOMPARE(rm.getUnit(), QString("x"));
+	QCOMPARE(rm.getValueForPosition(1), 0.5);
+	QCOMPARE(rm.getValueForPosition(8), 4.0);
+	QCOMPARE(rm.getValueForPosition(6), 3.0);
+	QCOMPARE(rm.getValueForPosition(7), 3.5);
+	QCOMPARE(rm.getValueForPosition(0), rm.getValueForPosition(1));
+	QCOMPARE(rm.getValueForPosition(9), rm.getValueForPosition(8));
+	QCOMPARE(rm.getValueForPositionUnclamped(6), 3.0);
+	QCOMPARE(rm.getValueForPositionUnclamped(0), 0.0);
+	QCOMPARE(rm.getValueForPositionUnclamped(-24), -12.0);
+	QCOMPARE(rm.getValueForPositionUnclamped(12), 6.0);
+    }
+
+    void linearDownBackward()
+    {
+	LinearRangeMapper rm(1, 8, 0.5, 4.0, "x", true);
+	QCOMPARE(rm.getUnit(), QString("x"));
+	QCOMPARE(rm.getValueForPosition(8), 0.5);
+	QCOMPARE(rm.getValueForPosition(1), 4.0);
+	QCOMPARE(rm.getValueForPosition(3), 3.0);
+	QCOMPARE(rm.getValueForPosition(2), 3.5);
+	QCOMPARE(rm.getValueForPosition(0), rm.getValueForPosition(1));
+	QCOMPARE(rm.getValueForPosition(9), rm.getValueForPosition(8));
+	QCOMPARE(rm.getValueForPositionUnclamped(3), 3.0);
+	QCOMPARE(rm.getValueForPositionUnclamped(9), 0.0);
+	QCOMPARE(rm.getValueForPositionUnclamped(33), -12.0);
+	QCOMPARE(rm.getValueForPositionUnclamped(-3), 6.0);
+    }
+
+    void logUpForward()
+    {
+	LogRangeMapper rm(3, 7, 10, 100000, "x", false);
+	QCOMPARE(rm.getUnit(), QString("x"));
+	QCOMPARE(rm.getPositionForValue(10.0), 3);
+	QCOMPARE(rm.getPositionForValue(100000.0), 7);
+	QCOMPARE(rm.getPositionForValue(1.0), 3);
+	QCOMPARE(rm.getPositionForValue(1000000.0), 7);
+	QCOMPARE(rm.getPositionForValue(1000.0), 5);
+	QCOMPARE(rm.getPositionForValue(900.0), 5);
+	QCOMPARE(rm.getPositionForValue(20000), 6);
+	QCOMPARE(rm.getPositionForValueUnclamped(1.0), 2);
+	QCOMPARE(rm.getPositionForValueUnclamped(1000000.0), 8);
+	QCOMPARE(rm.getPositionForValueUnclamped(1000.0), 5);
+    }
+
+    void logDownForward()
+    {
+	LogRangeMapper rm(3, 7, 10, 100000, "x", true);
+	QCOMPARE(rm.getUnit(), QString("x"));
+	QCOMPARE(rm.getPositionForValue(10.0), 7);
+	QCOMPARE(rm.getPositionForValue(100000.0), 3);
+	QCOMPARE(rm.getPositionForValue(1.0), 7);
+	QCOMPARE(rm.getPositionForValue(1000000.0), 3);
+	QCOMPARE(rm.getPositionForValue(1000.0), 5);
+	QCOMPARE(rm.getPositionForValue(900.0), 5);
+	QCOMPARE(rm.getPositionForValue(20000), 4);
+	QCOMPARE(rm.getPositionForValueUnclamped(1.0), 8);
+	QCOMPARE(rm.getPositionForValueUnclamped(1000000.0), 2);
+	QCOMPARE(rm.getPositionForValueUnclamped(1000.0), 5);
+    }
+
+    void logUpBackward()
+    {
+	LogRangeMapper rm(3, 7, 10, 100000, "x", false);
+	QCOMPARE(rm.getUnit(), QString("x"));
+	QCOMPARE(rm.getValueForPosition(3), 10.0);
+	QCOMPARE(rm.getValueForPosition(7), 100000.0);
+	QCOMPARE(rm.getValueForPosition(5), 1000.0);
+	QCOMPARE(rm.getValueForPosition(6), 10000.0);
+	QCOMPARE(rm.getValueForPosition(0), rm.getValueForPosition(3));
+	QCOMPARE(rm.getValueForPosition(9), rm.getValueForPosition(7));
+	QCOMPARE(rm.getValueForPositionUnclamped(2), 1.0);
+        QCOMPARE(rm.getValueForPositionUnclamped(8), 1000000.0);
+        QCOMPARE(rm.getValueForPositionUnclamped(5), 1000.0);
+    }
+
+    void logDownBackward()
+    {
+	LogRangeMapper rm(3, 7, 10, 100000, "x", true);
+	QCOMPARE(rm.getUnit(), QString("x"));
+	QCOMPARE(rm.getValueForPosition(7), 10.0);
+	QCOMPARE(rm.getValueForPosition(3), 100000.0);
+	QCOMPARE(rm.getValueForPosition(5), 1000.0);
+	QCOMPARE(rm.getValueForPosition(4), 10000.0);
+	QCOMPARE(rm.getValueForPosition(0), rm.getValueForPosition(3));
+	QCOMPARE(rm.getValueForPosition(9), rm.getValueForPosition(7));
+	QCOMPARE(rm.getValueForPositionUnclamped(8), 1.0);
+        QCOMPARE(rm.getValueForPositionUnclamped(2), 1000000.0);
+        QCOMPARE(rm.getValueForPositionUnclamped(5), 1000.0);
+    }
+
+    void interpolatingForward()
+    {
+	InterpolatingRangeMapper::CoordMap mappings;
+	mappings[1] = 10;
+	mappings[3] = 30;
+	mappings[5] = 70;
+	InterpolatingRangeMapper rm(mappings, "x");
+	QCOMPARE(rm.getUnit(), QString("x"));
+	QCOMPARE(rm.getPositionForValue(1.0), 10);
+	QCOMPARE(rm.getPositionForValue(0.0), 10);
+	QCOMPARE(rm.getPositionForValue(5.0), 70);
+	QCOMPARE(rm.getPositionForValue(6.0), 70);
+	QCOMPARE(rm.getPositionForValue(3.0), 30);
+	QCOMPARE(rm.getPositionForValue(2.5), 25);
+	QCOMPARE(rm.getPositionForValue(4.5), 60);
+	QCOMPARE(rm.getPositionForValueUnclamped(0.0), 0);
+	QCOMPARE(rm.getPositionForValueUnclamped(2.5), 25);
+	QCOMPARE(rm.getPositionForValueUnclamped(6.0), 90);
+    }
+
+    void interpolatingBackward()
+    {
+	InterpolatingRangeMapper::CoordMap mappings;
+	mappings[1] = 10;
+	mappings[3] = 30;
+	mappings[5] = 70;
+	InterpolatingRangeMapper rm(mappings, "x");
+	QCOMPARE(rm.getUnit(), QString("x"));
+	QCOMPARE(rm.getValueForPosition(10), 1.0);
+	QCOMPARE(rm.getValueForPosition(9), 1.0);
+	QCOMPARE(rm.getValueForPosition(70), 5.0);
+	QCOMPARE(rm.getValueForPosition(80), 5.0);
+	QCOMPARE(rm.getValueForPosition(30), 3.0);
+	QCOMPARE(rm.getValueForPosition(25), 2.5);
+	QCOMPARE(rm.getValueForPosition(60), 4.5);
+    }
+
+    void autoLinearForward()
+    {
+	AutoRangeMapper::CoordMap mappings;
+	mappings[0.5] = 1;
+	mappings[4.0] = 8;
+	AutoRangeMapper rm1(mappings, "x");
+	QCOMPARE(rm1.getUnit(), QString("x"));
+	QCOMPARE(rm1.getType(), AutoRangeMapper::StraightLine);
+	QCOMPARE(rm1.getPositionForValue(0.1), 1);
+	QCOMPARE(rm1.getPositionForValue(0.5), 1);
+	QCOMPARE(rm1.getPositionForValue(4.0), 8);
+	QCOMPARE(rm1.getPositionForValue(4.5), 8);
+	QCOMPARE(rm1.getPositionForValue(3.0), 6);
+	QCOMPARE(rm1.getPositionForValue(3.1), 6);
+	QCOMPARE(rm1.getPositionForValueUnclamped(0.1), 0);
+	QCOMPARE(rm1.getPositionForValueUnclamped(3.1), 6);
+	QCOMPARE(rm1.getPositionForValueUnclamped(4.5), 9);
+	mappings[3.0] = 6;
+	mappings[3.5] = 7;
+	AutoRangeMapper rm2(mappings, "x");
+	QCOMPARE(rm2.getUnit(), QString("x"));
+	QCOMPARE(rm2.getType(), AutoRangeMapper::StraightLine);
+	QCOMPARE(rm2.getPositionForValue(0.5), 1);
+	QCOMPARE(rm2.getPositionForValue(4.0), 8);
+	QCOMPARE(rm2.getPositionForValue(3.0), 6);
+	QCOMPARE(rm2.getPositionForValue(3.1), 6);
+    }
+
+    void autoLogForward()
+    {
+	AutoRangeMapper::CoordMap mappings;
+	mappings[10] = 3;
+	mappings[1000] = 5;
+	mappings[100000] = 7;
+	AutoRangeMapper rm1(mappings, "x");
+	QCOMPARE(rm1.getUnit(), QString("x"));
+	QCOMPARE(rm1.getType(), AutoRangeMapper::Logarithmic);
+	QCOMPARE(rm1.getPositionForValue(10.0), 3);
+	QCOMPARE(rm1.getPositionForValue(100000.0), 7);
+	QCOMPARE(rm1.getPositionForValue(1.0), 3);
+	QCOMPARE(rm1.getPositionForValue(1000000.0), 7);
+	QCOMPARE(rm1.getPositionForValue(1000.0), 5);
+	QCOMPARE(rm1.getPositionForValue(900.0), 5);
+	QCOMPARE(rm1.getPositionForValue(20000), 6);
+	QCOMPARE(rm1.getPositionForValueUnclamped(1.0), 2);
+	QCOMPARE(rm1.getPositionForValueUnclamped(900.0), 5);
+	QCOMPARE(rm1.getPositionForValueUnclamped(1000000.0), 8);
+	mappings[100] = 4;
+	AutoRangeMapper rm2(mappings, "x");
+	QCOMPARE(rm2.getUnit(), QString("x"));
+	QCOMPARE(rm2.getType(), AutoRangeMapper::Logarithmic);
+	QCOMPARE(rm2.getPositionForValue(10.0), 3);
+	QCOMPARE(rm2.getPositionForValue(100000.0), 7);
+	QCOMPARE(rm2.getPositionForValue(1.0), 3);
+	QCOMPARE(rm2.getPositionForValue(1000000.0), 7);
+	QCOMPARE(rm2.getPositionForValue(1000.0), 5);
+	QCOMPARE(rm2.getPositionForValue(900.0), 5);
+	QCOMPARE(rm2.getPositionForValue(20000), 6);
+    }
+
+    void autoInterpolatingForward()
+    {
+	AutoRangeMapper::CoordMap mappings;
+	mappings[1] = 10;
+	mappings[3] = 30;
+	mappings[5] = 70;
+	AutoRangeMapper rm(mappings, "x");
+	QCOMPARE(rm.getUnit(), QString("x"));
+	QCOMPARE(rm.getType(), AutoRangeMapper::Interpolating);
+	QCOMPARE(rm.getPositionForValue(1.0), 10);
+	QCOMPARE(rm.getPositionForValue(0.0), 10);
+	QCOMPARE(rm.getPositionForValue(5.0), 70);
+	QCOMPARE(rm.getPositionForValue(6.0), 70);
+	QCOMPARE(rm.getPositionForValue(3.0), 30);
+	QCOMPARE(rm.getPositionForValue(2.5), 25);
+	QCOMPARE(rm.getPositionForValue(4.5), 60);
+	QCOMPARE(rm.getPositionForValueUnclamped(0.0), 0);
+	QCOMPARE(rm.getPositionForValueUnclamped(5.0), 70);
+	QCOMPARE(rm.getPositionForValueUnclamped(6.0), 90);
+    }
+};
+
+#endif
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/test/main.cpp	Tue Mar 11 17:32:31 2014 +0000
@@ -0,0 +1,41 @@
+/* -*- 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 "TestRangeMapper.h"
+
+#include <QtTest>
+
+#include <iostream>
+
+int main(int argc, char *argv[])
+{
+    int good = 0, bad = 0;
+
+    QCoreApplication app(argc, argv);
+    app.setOrganizationName("Sonic Visualiser");
+    app.setApplicationName("test-svcore-base");
+
+    {
+	TestRangeMapper t;
+	if (QTest::qExec(&t, argc, argv) == 0) ++good;
+	else ++bad;
+    }
+
+    if (bad > 0) {
+	cerr << "\n********* " << bad << " test suite(s) failed!\n" << endl;
+	return 1;
+    } else {
+	cerr << "All tests passed" << endl;
+	return 0;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/test/test.pro	Tue Mar 11 17:32:31 2014 +0000
@@ -0,0 +1,53 @@
+
+TEMPLATE = app
+
+LIBS += -L../.. -L../../../dataquay -L../../release -L../../../dataquay/release -lsvcore -ldataquay
+
+win32-g++ {
+    INCLUDEPATH += ../../../sv-dependency-builds/win32-mingw/include
+    LIBS += -L../../../sv-dependency-builds/win32-mingw/lib
+}
+
+exists(../../config.pri) {
+    include(../../config.pri)
+}
+
+win* {
+    !exists(../../config.pri) {
+        DEFINES += HAVE_BZ2 HAVE_FFTW3 HAVE_FFTW3F HAVE_SNDFILE HAVE_SAMPLERATE HAVE_VAMP HAVE_VAMPHOSTSDK HAVE_RUBBERBAND HAVE_DATAQUAY HAVE_LIBLO HAVE_MAD HAVE_ID3TAG HAVE_PORTAUDIO_2_0
+        LIBS += -lbz2 -lrubberband -lvamp-hostsdk -lfftw3 -lfftw3f -lsndfile -lFLAC -logg -lvorbis -lvorbisenc -lvorbisfile -logg -lmad -lid3tag -lportaudio -lsamplerate -llo -lz -lsord-0 -lserd-0 -lwinmm -lws2_32
+    }
+}
+
+CONFIG += qt thread warn_on stl rtti exceptions console
+QT += network xml testlib
+QT -= gui
+
+TARGET = svcore-base-test
+
+DEPENDPATH += ../..
+INCLUDEPATH += ../..
+OBJECTS_DIR = o
+MOC_DIR = o
+
+HEADERS += TestRangeMapper.h
+SOURCES += main.cpp
+
+win* {
+//PRE_TARGETDEPS += ../../svcore.lib
+}
+!win* {
+PRE_TARGETDEPS += ../../libsvcore.a
+}
+
+!win32 {
+    !macx* {
+        QMAKE_POST_LINK=./$${TARGET}
+    }
+    macx* {
+        QMAKE_POST_LINK=./$${TARGET}.app/Contents/MacOS/$${TARGET}
+    }
+}
+
+win32:QMAKE_POST_LINK=./release/$${TARGET}.exe
+
--- a/data/fileio/test/main.cpp	Tue Mar 11 17:30:35 2014 +0000
+++ b/data/fileio/test/main.cpp	Tue Mar 11 17:32:31 2014 +0000
@@ -1,5 +1,16 @@
 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
-/* Copyright Chris Cannam - All Rights Reserved */
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2013 Chris Cannam.
+    
+    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 "AudioFileReaderTest.h"
 
--- a/data/fileio/test/test.pro	Tue Mar 11 17:30:35 2014 +0000
+++ b/data/fileio/test/test.pro	Tue Mar 11 17:32:31 2014 +0000
@@ -23,7 +23,7 @@
 QT += network xml testlib
 QT -= gui
 
-TARGET = svcore-test
+TARGET = svcore-data-fileio-test
 
 DEPENDPATH += ../../..
 INCLUDEPATH += ../../..
--- a/data/model/DenseThreeDimensionalModel.h	Tue Mar 11 17:30:35 2014 +0000
+++ b/data/model/DenseThreeDimensionalModel.h	Tue Mar 11 17:32:31 2014 +0000
@@ -83,6 +83,25 @@
     virtual QString getBinName(size_t n) const = 0;
 
     /**
+     * Return true if the bins have values as well as names. If this
+     * returns true, getBinValue() may be used to retrieve the values.
+     */
+    virtual bool hasBinValues() const { return false; }
+
+    /**
+     * Return the value of bin n, if any. This is a "vertical scale"
+     * value which does not vary from one column to the next. This is
+     * only meaningful if hasBinValues() returns true.
+     */
+    virtual float getBinValue(size_t n) const { return n; }
+
+    /**
+     * Obtain the name of the unit of the values returned from
+     * getBinValue(), if any.
+     */
+    virtual QString getBinValueUnit() const { return ""; }
+
+    /**
      * Estimate whether a logarithmic scale might be appropriate for
      * the value scale.
      */
--- a/data/model/EditableDenseThreeDimensionalModel.cpp	Tue Mar 11 17:30:35 2014 +0000
+++ b/data/model/EditableDenseThreeDimensionalModel.cpp	Tue Mar 11 17:32:31 2014 +0000
@@ -410,6 +410,37 @@
 }
 
 bool
+EditableDenseThreeDimensionalModel::hasBinValues() const
+{
+    return !m_binValues.empty();
+}
+
+float
+EditableDenseThreeDimensionalModel::getBinValue(size_t n) const
+{
+    if (n < m_binValues.size()) return m_binValues[n];
+    else return 0.f;
+}
+
+void
+EditableDenseThreeDimensionalModel::setBinValues(std::vector<float> values)
+{
+    m_binValues = values;
+}
+
+QString
+EditableDenseThreeDimensionalModel::getBinValueUnit() const
+{
+    return m_binValueUnit;
+}
+
+void
+EditableDenseThreeDimensionalModel::setBinValueUnit(QString unit)
+{
+    m_binValueUnit = unit;
+}
+
+bool
 EditableDenseThreeDimensionalModel::shouldUseLogValueScale() const
 {
     QReadLocker locker(&m_lock);
--- a/data/model/EditableDenseThreeDimensionalModel.h	Tue Mar 11 17:30:35 2014 +0000
+++ b/data/model/EditableDenseThreeDimensionalModel.h	Tue Mar 11 17:32:31 2014 +0000
@@ -127,10 +127,61 @@
      */
     virtual void setColumn(size_t x, const Column &values);
 
+    /**
+     * Return the name of bin n. This is a single label per bin that
+     * does not vary from one column to the next.
+     */
     virtual QString getBinName(size_t n) const;
+
+    /**
+     * Set the name of bin n.
+     */
     virtual void setBinName(size_t n, QString);
+
+    /**
+     * Set the names of all bins.
+     */
     virtual void setBinNames(std::vector<QString> names);
 
+    /**
+     * Return true if the bins have values as well as names. (The
+     * values may have been derived from the names, e.g. by parsing
+     * numbers from them.) If this returns true, getBinValue() may be
+     * used to retrieve the values.
+     */
+    virtual bool hasBinValues() const;
+
+    /**
+     * Return the value of bin n, if any. This is a "vertical scale"
+     * value which does not vary from one column to the next. This is
+     * only meaningful if hasBinValues() returns true.
+     */
+    virtual float getBinValue(size_t n) const;
+
+    /**
+     * Set the values of all bins (separate from their labels). These
+     * are "vertical scale" values which do not vary from one column
+     * to the next.
+     */
+    virtual void setBinValues(std::vector<float> values);
+
+    /**
+     * Obtain the name of the unit of the values returned from
+     * getBinValue(), if any.
+     */
+    virtual QString getBinValueUnit() const;
+
+    /**
+     * Set the name of the unit of the values return from
+     * getBinValue() if any.
+     */
+    virtual void setBinValueUnit(QString unit);
+
+    /**
+     * Return true if the distribution of values in the bins is such
+     * as to suggest a log scale (mapping to colour etc) may be better
+     * than a linear one.
+     */
     bool shouldUseLogValueScale() const;
 
     virtual void setCompletion(int completion, bool update = true);
@@ -162,6 +213,8 @@
     Column expandAndRetrieve(size_t index) const;
 
     std::vector<QString> m_binNames;
+    std::vector<float> m_binValues;
+    QString m_binValueUnit;
 
     size_t m_startFrame;
     size_t m_sampleRate;
--- a/transform/FeatureExtractionModelTransformer.cpp	Tue Mar 11 17:30:35 2014 +0000
+++ b/transform/FeatureExtractionModelTransformer.cpp	Tue Mar 11 17:32:31 2014 +0000
@@ -278,12 +278,11 @@
         //!!! the model rate to be the input model's rate, and adjust
         //!!! the resolution appropriately.  We can't properly display
         //!!! data with a higher resolution than the base model at all
-//	modelRate = size_t(m_descriptors[n]->sampleRate + 0.001);
         if (m_descriptors[n]->sampleRate > input->getSampleRate()) {
             modelResolution = 1;
         } else {
-            modelResolution = size_t(input->getSampleRate() /
-                                     m_descriptors[n]->sampleRate);
+            modelResolution = size_t(round(input->getSampleRate() /
+                                           m_descriptors[n]->sampleRate));
         }
 	break;
     }
@@ -852,7 +851,6 @@
 //             << ", m_descriptor->sampleRate = " << m_descriptor->sampleRate
 //             << ", inputRate = " << inputRate
 //             << " giving frame = ";
- 
         frame = lrintf((m_fixedRateFeatureNos[n] / m_descriptors[n]->sampleRate)
                        * int(inputRate));
     }
@@ -927,7 +925,7 @@
             }
         }
 
-		if (isOutput<FlexiNoteModel>(n)) { // GF: added for flexi note model
+        if (isOutput<FlexiNoteModel>(n)) { // GF: added for flexi note model
 
             float velocity = 100;
             if (feature.values.size() > index) {
@@ -995,7 +993,14 @@
             getConformingOutput<EditableDenseThreeDimensionalModel>(n);
 	if (!model) return;
 
-	model->setColumn(frame / model->getResolution(), values);
+//        cerr << "(note: model resolution = " << model->getResolution() << ")"
+//             << endl;
+
+        if (!feature.hasTimestamp && m_fixedRateFeatureNos[n] >= 0) {
+            model->setColumn(m_fixedRateFeatureNos[n], values);
+        } else {
+            model->setColumn(frame / model->getResolution(), values);
+        }
 
     } else {
         SVDEBUG << "FeatureExtractionModelTransformer::addFeature: Unknown output model type!" << endl;