changeset 905:1f94f3776158

Merge from 898:5821b64c6b26
author Chris Cannam
date Wed, 07 May 2014 15:17:43 +0100
parents 16c48a3db2a7 (diff) 5821b64c6b26 (current diff)
children 654990320867
files
diffstat 15 files changed, 933 insertions(+), 793 deletions(-) [+]
line wrap: on
line diff
--- a/base/RangeMapper.cpp	Sat Apr 26 22:22:17 2014 +0100
+++ b/base/RangeMapper.cpp	Wed May 07 15:17:43 2014 +0100
@@ -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	Sat Apr 26 22:22:17 2014 +0100
+++ b/base/RangeMapper.h	Wed May 07 15:17:43 2014 +0100
@@ -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
--- a/base/RealTime.cpp	Sat Apr 26 22:22:17 2014 +0100
+++ b/base/RealTime.cpp	Wed May 07 15:17:43 2014 +0100
@@ -442,7 +442,7 @@
 {
     if (time < zeroTime) return -realTime2Frame(-time, sampleRate);
     double s = time.sec + double(time.nsec + 1) / 1000000000.0;
-    return long(s * sampleRate);
+    return long(s * double(sampleRate));
 }
 
 RealTime
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/test/TestRangeMapper.h	Wed May 07 15:17:43 2014 +0100
@@ -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	Wed May 07 15:17:43 2014 +0100
@@ -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	Wed May 07 15:17:43 2014 +0100
@@ -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	Sat Apr 26 22:22:17 2014 +0100
+++ b/data/fileio/test/main.cpp	Wed May 07 15:17:43 2014 +0100
@@ -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	Sat Apr 26 22:22:17 2014 +0100
+++ b/data/fileio/test/test.pro	Wed May 07 15:17:43 2014 +0100
@@ -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	Sat Apr 26 22:22:17 2014 +0100
+++ b/data/model/DenseThreeDimensionalModel.h	Wed May 07 15:17:43 2014 +0100
@@ -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	Sat Apr 26 22:22:17 2014 +0100
+++ b/data/model/EditableDenseThreeDimensionalModel.cpp	Wed May 07 15:17:43 2014 +0100
@@ -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	Sat Apr 26 22:22:17 2014 +0100
+++ b/data/model/EditableDenseThreeDimensionalModel.h	Wed May 07 15:17:43 2014 +0100
@@ -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/data/model/SparseModel.h	Sat Apr 26 22:22:17 2014 +0100
+++ b/data/model/SparseModel.h	Wed May 07 15:17:43 2014 +0100
@@ -161,7 +161,7 @@
     { 
         QString s;
         for (PointListConstIterator i = m_points.begin(); i != m_points.end(); ++i) {
-            if (i->frame >= f0 && i->frame < f1) {
+            if (i->frame >= (long)f0 && i->frame < (long)f1) {
                 s += i->toDelimitedDataString(delimiter, m_sampleRate) + "\n";
             }
         }
--- a/rdf/SimpleSPARQLQuery.cpp	Sat Apr 26 22:22:17 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,639 +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 file copyright 2008 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 "SimpleSPARQLQuery.h"
-#include "base/ProgressReporter.h"
-#include "base/Profiler.h"
-
-#include <QMutex>
-#include <QMutexLocker>
-#include <QRegExp>
-
-#include <set>
-
-// Rather than including <redland.h> -- for some reason redland.h
-// includes <rasqal.h>, while the rasqal header actually gets
-// installed as <rasqal/rasqal.h> which breaks the inclusion all over
-// the place unless a very clever include path is set
-#include <rasqal/rasqal.h>
-#include <librdf.h>
-
-//#define DEBUG_SIMPLE_SPARQL_QUERY 1
-
-#include <iostream>
-
-using cerr;
-using endl;
-
-class WredlandWorldWrapper
-{
-public:
-    WredlandWorldWrapper();
-    ~WredlandWorldWrapper();
-
-    bool isOK() const;
-
-    bool loadUriIntoDefaultModel(QString uriString, QString &errorString);
-        
-    librdf_world *getWorld() { return m_world; }
-    const librdf_world *getWorld() const { return m_world; }
-        
-    librdf_model *getDefaultModel() { return m_defaultModel; }
-    const librdf_model *getDefaultModel() const { return m_defaultModel; }
-
-    librdf_model *getModel(QString fromUri);
-    void freeModel(QString forUri);
-
-private:
-    QMutex m_mutex;
-
-    librdf_world *m_world;
-    librdf_storage *m_defaultStorage;
-    librdf_model *m_defaultModel;
-
-    std::set<QString> m_defaultModelUris;
-
-    std::map<QString, librdf_storage *> m_ownStorageUris;
-    std::map<QString, librdf_model *> m_ownModelUris;
-
-    bool loadUri(librdf_model *model, QString uri, QString &errorString);
-};
-
-WredlandWorldWrapper::WredlandWorldWrapper() :
-    m_world(0), m_defaultStorage(0), m_defaultModel(0)
-{
-    m_world = librdf_new_world();
-    if (!m_world) {
-        cerr << "SimpleSPARQLQuery: ERROR: Failed to create LIBRDF world!" << endl;
-        return;
-    }
-    librdf_world_open(m_world);
-
-    m_defaultStorage = librdf_new_storage(m_world, "trees", NULL, NULL);
-    if (!m_defaultStorage) {
-        cerr << "SimpleSPARQLQuery: WARNING: Failed to initialise Redland trees datastore, falling back to memory store" << endl;
-        m_defaultStorage = librdf_new_storage(m_world, NULL, NULL, NULL);
-        if (!m_defaultStorage) {
-            cerr << "SimpleSPARQLQuery: ERROR: Failed to initialise Redland memory datastore" << endl;
-            return;
-        }                
-    }
-    m_defaultModel = librdf_new_model(m_world, m_defaultStorage, NULL);
-    if (!m_defaultModel) {
-        cerr << "SimpleSPARQLQuery: ERROR: Failed to initialise Redland data model" << endl;
-        return;
-    }
-}
-
-WredlandWorldWrapper::~WredlandWorldWrapper()
-{
-/*!!! This is a static singleton; destroying it while there are
-      queries outstanding can be problematic, it appears, and since
-      the storage is non-persistent there shouldn't be any need to
-      destroy it explicitly, except for the sake of tidiness.
-
-    while (!m_ownModelUris.empty()) {
-        librdf_free_model(m_ownModelUris.begin()->second);
-        m_ownModelUris.erase(m_ownModelUris.begin());
-    }
-    while (!m_ownStorageUris.empty()) {
-        librdf_free_storage(m_ownStorageUris.begin()->second);
-        m_ownStorageUris.erase(m_ownStorageUris.begin());
-    }
-    if (m_defaultModel) librdf_free_model(m_defaultModel);
-    if (m_defaultStorage) librdf_free_storage(m_defaultStorage);
-    if (m_world) librdf_free_world(m_world);
-*/
-}
-
-bool
-WredlandWorldWrapper::isOK() const {
-    return (m_defaultModel != 0); 
-}
-
-bool
-WredlandWorldWrapper::loadUriIntoDefaultModel(QString uriString, QString &errorString)
-{
-    QMutexLocker locker(&m_mutex);
-    
-    if (m_defaultModelUris.find(uriString) != m_defaultModelUris.end()) {
-        return true;
-    }
-    
-    if (loadUri(m_defaultModel, uriString, errorString)) {
-        m_defaultModelUris.insert(uriString);
-        return true;
-    } else {
-        return false;
-    }
-}
-
-librdf_model *
-WredlandWorldWrapper::getModel(QString fromUri)
-{
-    QMutexLocker locker(&m_mutex);
-    if (fromUri == "") {
-        return getDefaultModel();
-    }
-    if (m_ownModelUris.find(fromUri) != m_ownModelUris.end()) {
-        return m_ownModelUris[fromUri];
-    }
-    librdf_storage *storage = librdf_new_storage(m_world, "trees", NULL, NULL);
-    if (!storage) { // don't warn here, we probably already did it in main ctor
-        storage = librdf_new_storage(m_world, NULL, NULL, NULL);
-    }
-    librdf_model *model = librdf_new_model(m_world, storage, NULL);
-    if (!model) {
-        cerr << "SimpleSPARQLQuery: ERROR: Failed to create new model" << endl;
-        librdf_free_storage(storage);
-        return 0;
-    }
-    QString err;
-    if (!loadUri(model, fromUri, err)) {
-        cerr << "SimpleSPARQLQuery: ERROR: Failed to parse into new model: " << err << endl;
-        librdf_free_model(model);
-        librdf_free_storage(storage);
-        m_ownModelUris[fromUri] = 0;
-        return 0;
-    }
-    m_ownModelUris[fromUri] = model;
-    m_ownStorageUris[fromUri] = storage;
-    return model;
-}
-
-void
-WredlandWorldWrapper::freeModel(QString forUri)
-{
-#ifdef DEBUG_SIMPLE_SPARQL_QUERY
-    SVDEBUG << "SimpleSPARQLQuery::freeModel: Model uri = \"" << forUri << "\"" << endl;
-#endif
-
-    QMutexLocker locker(&m_mutex);
-    if (forUri == "") {
-        SVDEBUG << "SimpleSPARQLQuery::freeModel: ERROR: Can't free default model" << endl;
-        return;
-    }
-    if (m_ownModelUris.find(forUri) == m_ownModelUris.end()) {
-#ifdef DEBUG_SIMPLE_SPARQL_QUERY
-        SVDEBUG << "SimpleSPARQLQuery::freeModel: NOTE: Unknown or already-freed model (uri = \"" << forUri << "\")" << endl;
-#endif
-        return;
-    }
-
-    librdf_model *model = m_ownModelUris[forUri];
-    if (model) librdf_free_model(model);
-    m_ownModelUris.erase(forUri);
-
-    if (m_ownStorageUris.find(forUri) != m_ownStorageUris.end()) {
-        librdf_storage *storage = m_ownStorageUris[forUri];
-        if (storage) librdf_free_storage(storage);
-        m_ownStorageUris.erase(forUri);
-    }        
-}
-
-bool
-WredlandWorldWrapper::loadUri(librdf_model *model, QString uri, QString &errorString)
-{
-    librdf_uri *luri = librdf_new_uri
-        (m_world, (const unsigned char *)uri.toUtf8().data());
-    if (!luri) {
-        errorString = "Failed to construct librdf_uri!";
-        return false;
-    }
-    
-    librdf_parser *parser = librdf_new_parser(m_world, "guess", NULL, NULL);
-    if (!parser) {
-        errorString = "Failed to initialise Redland parser";
-        return false;
-    }
-
-#ifdef DEBUG_SIMPLE_SPARQL_QUERY    
-    cerr << "About to parse \"" << uri << "\"" << endl;
-#endif
-    
-    Profiler p("SimpleSPARQLQuery: Parse URI into LIBRDF model");
-    
-    if (librdf_parser_parse_into_model(parser, luri, NULL, model)) {
-        
-        errorString = QString("Failed to parse RDF from URI \"%1\"")
-            .arg(uri);
-        librdf_free_parser(parser);
-        return false;
-        
-    } else {
-        
-        librdf_free_parser(parser);
-        return true;
-    }
-}        
-
-
-class SimpleSPARQLQuery::Impl
-{
-public:
-    Impl(SimpleSPARQLQuery::QueryType, QString query);
-    ~Impl();
-
-    static bool addSourceToModel(QString sourceUri);
-    static void closeSingleSource(QString sourceUri);
-
-    void setProgressReporter(ProgressReporter *reporter) { m_reporter = reporter; }
-    bool wasCancelled() const { return m_cancelled; }
-
-    ResultList execute();
-
-    bool isOK() const;
-    QString getErrorString() const;
-
-protected:
-    static QMutex m_mutex;
-
-    static WredlandWorldWrapper *m_redland;
-
-    ResultList executeDirectParser();
-    ResultList executeDatastore();
-    ResultList executeFor(QString modelUri);
-
-    QueryType m_type;
-    QString m_query;
-    QString m_errorString;
-    ProgressReporter *m_reporter;
-    bool m_cancelled;
-};
-
-WredlandWorldWrapper *SimpleSPARQLQuery::Impl::m_redland = 0;
-
-QMutex SimpleSPARQLQuery::Impl::m_mutex;
-
-SimpleSPARQLQuery::SimpleSPARQLQuery(QueryType type, QString query) :
-    m_impl(new Impl(type, query))
-{
-}
-
-SimpleSPARQLQuery::~SimpleSPARQLQuery() 
-{
-    delete m_impl;
-}
-
-void
-SimpleSPARQLQuery::setProgressReporter(ProgressReporter *reporter)
-{
-    m_impl->setProgressReporter(reporter);
-}
-
-bool
-SimpleSPARQLQuery::wasCancelled() const
-{
-    return m_impl->wasCancelled();
-}
-
-SimpleSPARQLQuery::ResultList
-SimpleSPARQLQuery::execute()
-{
-    return m_impl->execute();
-}
-
-bool
-SimpleSPARQLQuery::isOK() const
-{
-    return m_impl->isOK();
-}
-
-QString
-SimpleSPARQLQuery::getErrorString() const
-{
-    return m_impl->getErrorString();
-}
-
-bool
-SimpleSPARQLQuery::addSourceToModel(QString sourceUri)
-{
-    return SimpleSPARQLQuery::Impl::addSourceToModel(sourceUri);
-}
-
-void
-SimpleSPARQLQuery::closeSingleSource(QString sourceUri)
-{
-    SimpleSPARQLQuery::Impl::closeSingleSource(sourceUri);
-}
-
-SimpleSPARQLQuery::Impl::Impl(QueryType type, QString query) :
-    m_type(type),
-    m_query(query),
-    m_reporter(0),
-    m_cancelled(false)
-{
-}
-
-SimpleSPARQLQuery::Impl::~Impl()
-{
-}
-
-bool
-SimpleSPARQLQuery::Impl::isOK() const
-{
-    return (m_errorString == "");
-}
-
-QString
-SimpleSPARQLQuery::Impl::getErrorString() const
-{
-    return m_errorString;
-}
-
-SimpleSPARQLQuery::ResultList
-SimpleSPARQLQuery::Impl::execute()
-{
-    ResultList list;
-
-    QMutexLocker locker(&m_mutex);
-
-    if (!m_redland) {
-        m_redland = new WredlandWorldWrapper();
-    }
-
-    if (!m_redland->isOK()) {
-        cerr << "ERROR: SimpleSPARQLQuery::execute: Failed to initialise Redland datastore" << endl;
-        return list;
-    }
-
-    if (m_type == QueryFromSingleSource) {
-        return executeDirectParser();
-    } else {
-        return executeDatastore();
-    }
-
-#ifdef DEBUG_SIMPLE_SPARQL_QUERY
-    if (m_errorString != "") {
-        cerr << "SimpleSPARQLQuery::execute: error returned: \""
-                  << m_errorString << "\"" << endl;
-    }
-#endif
-}
-
-SimpleSPARQLQuery::ResultList
-SimpleSPARQLQuery::Impl::executeDirectParser()
-{
-#ifdef DEBUG_SIMPLE_SPARQL_QUERY
-    SVDEBUG << "SimpleSPARQLQuery::executeDirectParser: Query is: \"" << m_query << "\"" << endl;
-#endif
-
-    ResultList list;
-
-    Profiler profiler("SimpleSPARQLQuery::executeDirectParser");
-
-    static QRegExp fromRE("from\\s+<([^>]+)>", Qt::CaseInsensitive);
-    QString fromUri;
-
-    if (fromRE.indexIn(m_query) < 0) {
-        SVDEBUG << "SimpleSPARQLQuery::executeDirectParser: Query contains no FROM clause, nothing to parse from" << endl;
-        return list;
-    } else {
-        fromUri = fromRE.cap(1);
-#ifdef DEBUG_SIMPLE_SPARQL_QUERY
-        SVDEBUG << "SimpleSPARQLQuery::executeDirectParser: FROM URI is <"
-                  << fromUri << ">" << endl;
-#endif
-    }
-
-    return executeFor(fromUri);
-}
-
-SimpleSPARQLQuery::ResultList
-SimpleSPARQLQuery::Impl::executeDatastore()
-{
-#ifdef DEBUG_SIMPLE_SPARQL_QUERY
-    SVDEBUG << "SimpleSPARQLQuery::executeDatastore: Query is: \"" << m_query << "\"" << endl;
-#endif
-
-    ResultList list;
-
-    Profiler profiler("SimpleSPARQLQuery::executeDatastore");
-
-    return executeFor("");
-}
-
-SimpleSPARQLQuery::ResultList
-SimpleSPARQLQuery::Impl::executeFor(QString modelUri)
-{
-    ResultList list;
-    librdf_query *query;
-
-#ifdef DEBUG_SIMPLE_SPARQL_QUERY
-    static std::map<QString, int> counter;
-    if (counter.find(m_query) == counter.end()) counter[m_query] = 1;
-    else ++counter[m_query];
-    cerr << "Counter for this query: " << counter[m_query] << endl;
-    cerr << "Base URI is: \"" << modelUri << "\"" << endl;
-#endif
-
-    {
-        Profiler p("SimpleSPARQLQuery: Prepare LIBRDF query");
-        query = librdf_new_query
-            (m_redland->getWorld(), "sparql", NULL,
-             (const unsigned char *)m_query.toUtf8().data(), NULL);
-    }
-    
-    if (!query) {
-        m_errorString = "Failed to construct query";
-        return list;
-    }
-
-    librdf_query_results *results;
-    {
-        Profiler p("SimpleSPARQLQuery: Execute LIBRDF query");
-        results = librdf_query_execute(query, m_redland->getModel(modelUri));
-    }
-
-    if (!results) {
-        m_errorString = "RDF query failed";
-        librdf_free_query(query);
-        return list;
-    }
-
-    if (!librdf_query_results_is_bindings(results)) {
-        m_errorString = "RDF query returned non-bindings results";
-        librdf_free_query_results(results);
-        librdf_free_query(query);
-        return list;
-    }
-    
-    int resultCount = 0;
-    int resultTotal = librdf_query_results_get_count(results); // probably wrong
-    m_cancelled = false;
-
-    while (!librdf_query_results_finished(results)) {
-
-        int count = librdf_query_results_get_bindings_count(results);
-
-        KeyValueMap resultmap;
-
-        for (int i = 0; i < count; ++i) {
-
-            const char *name =
-                librdf_query_results_get_binding_name(results, i);
-
-            if (!name) {
-                cerr << "WARNING: Result " << i << " of query has no name" << endl;
-                continue;
-            }
-
-            librdf_node *node =
-                librdf_query_results_get_binding_value(results, i);
-
-            QString key = (const char *)name;
-
-            if (!node) {
-#ifdef DEBUG_SIMPLE_SPARQL_QUERY
-                cerr << i << ". " << key << " -> (nil)" << endl;
-#endif
-                resultmap[key] = Value();
-                continue;
-            }
-
-            ValueType type = LiteralValue;
-            QString text;
-
-            if (librdf_node_is_resource(node)) {
-
-                type = URIValue;
-                librdf_uri *uri = librdf_node_get_uri(node);
-                const char *us = (const char *)librdf_uri_as_string(uri);
-
-                if (!us) {
-                    cerr << "WARNING: Result " << i << " of query claims URI type, but has null URI" << endl;
-                } else {
-                    text = us;
-                }
-
-            } else if (librdf_node_is_literal(node)) {
-
-                type = LiteralValue;
-
-                const char *lit = (const char *)librdf_node_get_literal_value(node);
-                if (!lit) {
-                    cerr << "WARNING: Result " << i << " of query claims literal type, but has no literal" << endl;
-                } else {
-                    text = lit;
-                }
-
-            } else if (librdf_node_is_blank(node)) {
-
-                type = BlankValue;
-
-                const char *lit = (const char *)librdf_node_get_literal_value(node);
-                if (lit) text = lit;
-
-            } else {
-
-                cerr << "SimpleSPARQLQuery: LIBRDF query returned unknown node type (not resource, literal, or blank)" << endl;
-            }
-
-#ifdef DEBUG_SIMPLE_SPARQL_QUERY
-            cerr << i << ". " << key << " -> " << text << " (type " << type << ")" << endl;
-#endif
-
-            resultmap[key] = Value(type, text);
-
-//            librdf_free_node(node);
-        }
-
-        list.push_back(resultmap);
-
-        librdf_query_results_next(results);
-
-        resultCount++;
-
-        if (m_reporter) {
-            if (resultCount >= resultTotal) {
-                if (m_reporter->isDefinite()) m_reporter->setDefinite(false);
-                m_reporter->setProgress(resultCount);
-            } else {
-                m_reporter->setProgress((resultCount * 100) / resultTotal);
-            }
-
-            if (m_reporter->wasCancelled()) {
-                m_cancelled = true;
-                break;
-            }
-        }
-    }
-
-    librdf_free_query_results(results);
-    librdf_free_query(query);
-
-#ifdef DEBUG_SIMPLE_SPARQL_QUERY
-    SVDEBUG << "SimpleSPARQLQuery::executeDatastore: All results retrieved (" << resultCount << " of them)" << endl;
-#endif
-
-    return list;
-}
-
-bool
-SimpleSPARQLQuery::Impl::addSourceToModel(QString sourceUri)
-{
-    QString err;
-
-    QMutexLocker locker(&m_mutex);
-
-    if (!m_redland) {
-        m_redland = new WredlandWorldWrapper();
-    }
-
-    if (!m_redland->isOK()) {
-        cerr << "SimpleSPARQLQuery::addSourceToModel: Failed to initialise Redland datastore" << endl;
-        return false;
-    }
-
-    if (!m_redland->loadUriIntoDefaultModel(sourceUri, err)) {
-        cerr << "SimpleSPARQLQuery::addSourceToModel: Failed to add source URI \"" << sourceUri << ": " << err << endl;
-        return false;
-    }
-    return true;
-}
-
-void
-SimpleSPARQLQuery::Impl::closeSingleSource(QString sourceUri)
-{
-    QMutexLocker locker(&m_mutex);
-
-    m_redland->freeModel(sourceUri);
-}
-
-SimpleSPARQLQuery::Value
-SimpleSPARQLQuery::singleResultQuery(QueryType type,
-                                     QString query, QString binding)
-{
-    SimpleSPARQLQuery q(type, query);
-    ResultList results = q.execute();
-    if (!q.isOK()) {
-        cerr << "SimpleSPARQLQuery::singleResultQuery: ERROR: "
-             << q.getErrorString() << endl;
-        return Value();
-    }
-    if (results.empty()) {
-        return Value();
-    }
-    for (int i = 0; i < results.size(); ++i) {
-        if (results[i].find(binding) != results[i].end() &&
-            results[i][binding].type != NoValue) {
-            return results[i][binding];
-        }
-    }
-    return Value();
-}
-
-
-
--- a/rdf/SimpleSPARQLQuery.h	Sat Apr 26 22:22:17 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,117 +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 file copyright 2008 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 _SIMPLE_SPARQL_QUERY_H_
-#define _SIMPLE_SPARQL_QUERY_H_
-
-#ifdef NOT_DEFINED
-
-#include <QString>
-#include <map>
-#include <vector>
-
-#include "base/Debug.h"
-
-class ProgressReporter;
-
-class SimpleSPARQLQuery
-{
-public:
-    enum ValueType { NoValue, URIValue, LiteralValue, BlankValue };
-
-    struct Value {
-        Value() : type(NoValue), value() { }
-        Value(ValueType t, QString v) : type(t), value(v) { }
-        ValueType type;
-        QString value;
-    };
-
-    typedef std::map<QString, Value> KeyValueMap;
-    typedef std::vector<KeyValueMap> ResultList;
-
-    /**
-     * QueryType specifies the context in which the query will be
-     * evaluated.  SimpleSPARQLQuery maintains a general global data
-     * model, into which data can be loaded using addSourceToModel(),
-     * as well as permitting one-time queries directly on data sources
-     * identified by URL.
-     *
-     * The query type QueryFromModel indicates a query to be evaluated
-     * over the general global model; the query type
-     * QueryFromSingleSource indicates that the query should be
-     * evaluated in the context of a model generated solely by parsing
-     * the FROM url found in the query.
-     *
-     * Even in QueryFromSingleSource mode, the parsed data remains in
-     * memory and will be reused in subsequent queries with the same
-     * mode and FROM url.  To release data loaded in this way once all
-     * queries across it are complete, pass the said FROM url to
-     * closeSingleSource().
-     */
-    enum QueryType {
-        QueryFromModel,
-        QueryFromSingleSource
-    };
-
-    /**
-     * Construct a query of the given type (indicating the data model
-     * context for the query) using the given SPARQL query content.
-     */
-    SimpleSPARQLQuery(QueryType type, QString query);
-    ~SimpleSPARQLQuery();
-
-    /**
-     * Add the given URI to the general global model used for
-     * QueryFromModel queries.
-     */
-    static bool addSourceToModel(QString sourceUri);
-
-    /**
-     * Release any data that has been loaded from the given source as
-     * part of a QueryFromSingleSource query with this source in the
-     * FROM clause.  Note this will not prevent any subsequent queries
-     * on the source from working -- it will just make them slower as
-     * the data will need to be re-parsed.
-     */
-    static void closeSingleSource(QString sourceUri);
-
-    void setProgressReporter(ProgressReporter *reporter);
-    bool wasCancelled() const;
-    
-    ResultList execute();
-
-    bool isOK() const;
-    QString getErrorString() const;
-
-    /**
-     * Construct and execute a query, and return the first result
-     * value for the given binding.
-     */
-    static Value singleResultQuery(QueryType type,
-                                   QString query,
-                                   QString binding);
-
-protected:
-    class Impl;
-    Impl *m_impl;
-
-private:
-    SimpleSPARQLQuery(const SimpleSPARQLQuery &); // not provided
-    SimpleSPARQLQuery &operator=(const SimpleSPARQLQuery &); // not provided
-};
-
-#endif
-
-#endif
--- a/transform/FeatureExtractionModelTransformer.cpp	Sat Apr 26 22:22:17 2014 +0100
+++ b/transform/FeatureExtractionModelTransformer.cpp	Wed May 07 15:17:43 2014 +0100
@@ -228,12 +228,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_descriptor->sampleRate + 0.001);
         if (m_descriptor->sampleRate > input->getSampleRate()) {
             modelResolution = 1;
         } else {
-            modelResolution = size_t(input->getSampleRate() /
-                                     m_descriptor->sampleRate);
+            modelResolution = size_t(round(input->getSampleRate() /
+                                           m_descriptor->sampleRate));
         }
 	break;
     }
@@ -638,7 +637,7 @@
 
 void
 FeatureExtractionModelTransformer::addFeature(size_t blockFrame,
-					     const Vamp::Plugin::Feature &feature)
+                                              const Vamp::Plugin::Feature &feature)
 {
     size_t inputRate = m_input.getModel()->getSampleRate();
 
@@ -653,7 +652,7 @@
 	binCount = m_descriptor->binCount;
     }
 
-    size_t frame = blockFrame;
+    int frame = blockFrame;
 
     if (m_descriptor->sampleType ==
 	Vamp::Plugin::OutputDescriptor::VariableSampleRate) {
@@ -678,11 +677,28 @@
             m_fixedRateFeatureNo =
                 lrint(ts.toDouble() * m_descriptor->sampleRate);
         }
- 
+
+//        cerr << "m_fixedRateFeatureNo = " << m_fixedRateFeatureNo 
+//             << ", m_descriptor->sampleRate = " << m_descriptor->sampleRate
+//             << ", inputRate = " << inputRate
+//             << " giving frame = ";
+
         frame = lrintf((m_fixedRateFeatureNo / m_descriptor->sampleRate)
-                       * inputRate);
+                       * int(inputRate));
+
+//        cerr << frame << endl;
     }
-	
+
+    if (frame < 0) {
+        cerr
+            << "WARNING: FeatureExtractionModelTransformer::addFeature: "
+            << "Negative frame counts are not supported (frame = " << frame
+            << " from timestamp " << feature.timestamp
+            << "), dropping feature" 
+            << endl;
+        return;
+    }
+
     // Rather than repeat the complicated tests from the constructor
     // to determine what sort of model we must be adding the features
     // to, we instead test what sort of model the constructor decided
@@ -784,7 +800,14 @@
             getConformingOutput<EditableDenseThreeDimensionalModel>();
 	if (!model) return;
 
-	model->setColumn(frame / model->getResolution(), values);
+//        cerr << "(note: model resolution = " << model->getResolution() << ")"
+//             << endl;
+
+        if (!feature.hasTimestamp && m_fixedRateFeatureNo >= 0) {
+            model->setColumn(m_fixedRateFeatureNo, values);
+        } else {
+            model->setColumn(frame / model->getResolution(), values);
+        }
 
     } else {
         SVDEBUG << "FeatureExtractionModelTransformer::addFeature: Unknown output model type!" << endl;