changeset 1615:24dc8cb42755 single-point

Rename a number of classes and methods (including Point -> Event); comments
author Chris Cannam
date Thu, 07 Mar 2019 15:44:09 +0000
parents 2e14a7876945
children de446dd905e6
files base/Clipboard.cpp base/Clipboard.h base/Event.h base/EventSeries.h base/NoteData.h base/Point.h base/PointSeries.h base/test/TestEventSeries.h base/test/TestPointSeries.h base/test/files.pri base/test/svcore-base-test.cpp data/fileio/MIDIFileWriter.cpp data/model/FlexiNoteModel.h data/model/NoteData.h data/model/NoteModel.h data/model/SparseOneDimensionalModel.h files.pri
diffstat 17 files changed, 824 insertions(+), 723 deletions(-) [+]
line wrap: on
line diff
--- a/base/Clipboard.cpp	Thu Mar 07 14:35:57 2019 +0000
+++ b/base/Clipboard.cpp	Thu Mar 07 15:44:09 2019 +0000
@@ -30,20 +30,20 @@
     return m_points.empty();
 }
 
-const PointVector &
+const EventVector &
 Clipboard::getPoints() const
 {
     return m_points;
 }
 
 void
-Clipboard::setPoints(const PointVector &pl)
+Clipboard::setPoints(const EventVector &pl)
 {
     m_points = pl;
 }
 
 void
-Clipboard::addPoint(const Point &point)
+Clipboard::addPoint(const Event &point)
 {
     m_points.push_back(point);
 }
@@ -51,9 +51,9 @@
 bool
 Clipboard::haveReferenceFrames() const
 {
-    for (PointVector::const_iterator i = m_points.begin();
+    for (EventVector::const_iterator i = m_points.begin();
          i != m_points.end(); ++i) {
-        if (i->haveReferenceFrame()) return true;
+        if (i->hasReferenceFrame()) return true;
     } 
     return false;
 }
@@ -61,7 +61,7 @@
 bool
 Clipboard::referenceFramesDiffer() const
 {
-    for (PointVector::const_iterator i = m_points.begin();
+    for (EventVector::const_iterator i = m_points.begin();
          i != m_points.end(); ++i) {
         if (i->referenceFrameDiffers()) return true;
     } 
--- a/base/Clipboard.h	Thu Mar 07 14:35:57 2019 +0000
+++ b/base/Clipboard.h	Thu Mar 07 15:44:09 2019 +0000
@@ -18,7 +18,7 @@
 
 #include <vector>
 
-#include "Point.h"
+#include "Event.h"
 
 class Clipboard
 {
@@ -28,15 +28,15 @@
 
     void clear();
     bool empty() const;
-    const PointVector &getPoints() const;
-    void setPoints(const PointVector &points);
-    void addPoint(const Point &point);
+    const EventVector &getPoints() const;
+    void setPoints(const EventVector &points);
+    void addPoint(const Event &point);
 
     bool haveReferenceFrames() const;
     bool referenceFramesDiffer() const;
 
 protected:
-     PointVector m_points;
+     EventVector m_points;
 };
 
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/Event.h	Thu Mar 07 15:44:09 2019 +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 file copyright 2006 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.
+*/
+
+#ifndef SV_EVENT_H
+#define SV_EVENT_H
+
+#include "BaseTypes.h"
+#include "NoteData.h"
+#include "XmlExportable.h"
+
+#include <vector>
+#include <stdexcept>
+
+#include <QString>
+
+/**
+ * An immutable type used for point and event representation in sparse
+ * models, as well as for interchange within the clipboard. An event
+ * always has a frame and (possibly empty) label, and optionally has
+ * numerical value, level, duration in frames, and a mapped reference
+ * frame. Event has an operator< defining a total ordering, by frame
+ * first and then by the other properties.
+ * 
+ * Event is based on the Clipboard::Point type up to SV v3.2.1 and is
+ * intended also to replace the custom point types previously found in
+ * sparse models.
+ */
+class Event
+{
+public:
+    Event(sv_frame_t frame) :
+        m_haveValue(false), m_haveLevel(false), m_haveReferenceFrame(false),
+        m_value(0.f), m_level(0.f), m_frame(frame),
+        m_duration(0), m_referenceFrame(0), m_label() { }
+        
+    Event(sv_frame_t frame, QString label) :
+        m_haveValue(false), m_haveLevel(false), m_haveReferenceFrame(false),
+        m_value(0.f), m_level(0.f), m_frame(frame),
+        m_duration(0), m_referenceFrame(0), m_label(label) { }
+        
+    Event(sv_frame_t frame, float value, QString label) :
+        m_haveValue(true), m_haveLevel(false), m_haveReferenceFrame(false),
+        m_value(value), m_level(0.f), m_frame(frame),
+        m_duration(0), m_referenceFrame(0), m_label(label) { }
+        
+    Event(sv_frame_t frame, float value, sv_frame_t duration, QString label) :
+        m_haveValue(true), m_haveLevel(false), m_haveReferenceFrame(false),
+        m_value(value), m_level(0.f), m_frame(frame),
+        m_duration(duration), m_referenceFrame(0), m_label(label) {
+        if (m_duration < 0) throw std::logic_error("duration must be >= 0");
+    }
+        
+    Event(sv_frame_t frame, float value, sv_frame_t duration,
+          float level, QString label) :
+        m_haveValue(true), m_haveLevel(true), m_haveReferenceFrame(false),
+        m_value(value), m_level(level), m_frame(frame),
+        m_duration(duration), m_referenceFrame(0), m_label(label) {
+        if (m_duration < 0) throw std::logic_error("duration must be >= 0");
+    }
+
+    Event(const Event &event) =default;
+    Event &operator=(const Event &event) =default;
+    Event &operator=(Event &&event) =default;
+    
+    sv_frame_t getFrame() const { return m_frame; }
+
+    Event withFrame(sv_frame_t frame) const {
+        Event p(*this);
+        p.m_frame = frame;
+        return p;
+    }
+    
+    bool hasValue() const { return m_haveValue; }
+    float getValue() const { return m_value; }
+    
+    Event withValue(float value) const {
+        Event p(*this);
+        p.m_haveValue = true;
+        p.m_value = value;
+        return p;
+    }
+    Event withoutValue() const {
+        Event p(*this);
+        p.m_haveValue = false;
+        p.m_value = 0.f;
+        return p;
+    }
+    
+    bool hasDuration() const { return m_duration != 0; }
+    sv_frame_t getDuration() const { return m_duration; }
+
+    Event withDuration(sv_frame_t duration) const {
+        Event p(*this);
+        p.m_duration = duration;
+        if (duration < 0) throw std::logic_error("duration must be >= 0");
+        return p;
+    }
+    Event withoutDuration() const {
+        Event p(*this);
+        p.m_duration = 0;
+        return p;
+    }
+    
+    QString getLabel() const { return m_label; }
+
+    Event withLabel(QString label) const {
+        Event p(*this);
+        p.m_label = label;
+        return p;
+    }
+    
+    bool hasLevel() const { return m_haveLevel; }
+    float getLevel() const { return m_level; }
+
+    Event withLevel(float level) const {
+        Event p(*this);
+        p.m_haveLevel = true;
+        p.m_level = level;
+        return p;
+    }
+    Event withoutLevel() const {
+        Event p(*this);
+        p.m_haveLevel = false;
+        p.m_level = 0.f;
+        return p;
+    }
+    
+    bool hasReferenceFrame() const { return m_haveReferenceFrame; }
+    sv_frame_t getReferenceFrame() const { return m_referenceFrame; }
+        
+    bool referenceFrameDiffers() const { // from event frame
+        return m_haveReferenceFrame && (m_referenceFrame != m_frame);
+    }
+    
+    Event withReferenceFrame(sv_frame_t frame) const {
+        Event p(*this);
+        p.m_haveReferenceFrame = true;
+        p.m_referenceFrame = frame;
+        return p;
+    }
+    Event withoutReferenceFrame() const {
+        Event p(*this);
+        p.m_haveReferenceFrame = false;
+        p.m_referenceFrame = 0;
+        return p;
+    }
+
+    bool operator==(const Event &p) const {
+
+        if (m_frame != p.m_frame) return false;
+        if (m_duration != p.m_duration) return false;
+
+        if (m_haveValue != p.m_haveValue) return false;
+        if (m_haveValue && (m_value != p.m_value)) return false;
+
+        if (m_haveLevel != p.m_haveLevel) return false;
+        if (m_haveLevel && (m_level != p.m_level)) return false;
+
+        if (m_haveReferenceFrame != p.m_haveReferenceFrame) return false;
+        if (m_haveReferenceFrame &&
+            (m_referenceFrame != p.m_referenceFrame)) return false;
+        
+        if (m_label != p.m_label) return false;
+        
+        return true;
+    }
+
+    bool operator<(const Event &p) const {
+
+        if (m_frame != p.m_frame) return m_frame < p.m_frame;
+        if (m_duration != p.m_duration) return m_duration < p.m_duration;
+
+        // events without a property sort before events with that property
+
+        if (m_haveValue != p.m_haveValue) return !m_haveValue;
+        if (m_haveValue && (m_value != p.m_value)) return m_value < p.m_value;
+        
+        if (m_haveLevel != p.m_haveLevel) return !m_haveLevel;
+        if (m_haveLevel && (m_level != p.m_level)) return m_level < p.m_level;
+
+        if (m_haveReferenceFrame != p.m_haveReferenceFrame) {
+            return !m_haveReferenceFrame;
+        }
+        if (m_haveReferenceFrame && (m_referenceFrame != p.m_referenceFrame)) {
+            return m_referenceFrame < p.m_referenceFrame;
+        }
+        
+        return m_label < p.m_label;
+    }
+
+    void toXml(QTextStream &stream,
+               QString indent = "",
+               QString extraAttributes = "") const {
+
+        // For I/O purposes these are points, not events
+        stream << indent << QString("<point frame=\"%1\" ").arg(m_frame);
+        if (m_haveValue) stream << QString("value=\"%1\" ").arg(m_value);
+        if (m_duration) stream << QString("duration=\"%1\" ").arg(m_duration);
+        if (m_haveLevel) stream << QString("level=\"%1\" ").arg(m_level);
+        if (m_haveReferenceFrame) stream << QString("referenceFrame=\"%1\" ")
+                                      .arg(m_referenceFrame);
+        stream << QString("label=\"%1\" ")
+            .arg(XmlExportable::encodeEntities(m_label));
+        stream << extraAttributes << ">\n";
+    }
+
+    QString toXmlString(QString indent = "",
+                        QString extraAttributes = "") const {
+        QString s;
+        QTextStream out(&s);
+        toXml(out, indent, extraAttributes);
+        out.flush();
+        return s;
+    }
+
+    NoteData toNoteData(sv_samplerate_t sampleRate, bool valueIsMidiPitch) {
+
+        sv_frame_t duration;
+        if (m_duration > 0) {
+            duration = m_duration;
+        } else {
+            duration = sv_frame_t(sampleRate / 6); // arbitrary short duration
+        }
+
+        int midiPitch;
+        float frequency = 0.f;
+        if (m_haveValue) {
+            if (valueIsMidiPitch) {
+                midiPitch = int(roundf(m_value));
+            } else {
+                frequency = m_value;
+                midiPitch = Pitch::getPitchForFrequency(frequency);
+            }
+        } else {
+            midiPitch = 64;
+            valueIsMidiPitch = true;
+        }
+
+        int velocity = 100;
+        if (m_haveLevel) {
+            if (m_level > 0.f && m_level <= 1.f) {
+                velocity = int(roundf(m_level * 127.f));
+            }
+        }
+
+        NoteData n(m_frame, duration, midiPitch, velocity);
+        n.isMidiPitchQuantized = valueIsMidiPitch;
+        if (!valueIsMidiPitch) {
+            n.frequency = frequency;
+        }
+
+        return n;
+    }
+    
+private:
+    // The order of fields here is chosen to minimise overall size of struct.
+    // We potentially store very many of these objects.
+    // If you change something, check what difference it makes to packing.
+    bool m_haveValue : 1;
+    bool m_haveLevel : 1;
+    bool m_haveReferenceFrame : 1;
+    float m_value;
+    float m_level;
+    sv_frame_t m_frame;
+    sv_frame_t m_duration;
+    sv_frame_t m_referenceFrame;
+    QString m_label;
+};
+
+typedef std::vector<Event> EventVector;
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/EventSeries.h	Thu Mar 07 15:44:09 2019 +0000
@@ -0,0 +1,241 @@
+/* -*- 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 SV_EVENT_SERIES_H
+#define SV_EVENT_SERIES_H
+
+#include "Event.h"
+
+#include <set>
+
+//#define DEBUG_EVENT_SERIES 1
+
+/**
+ * Container storing a series of events, with or without durations,
+ * and supporting the ability to query which events span a given frame
+ * time. To that end, in addition to the series of events, it stores a
+ * series of "seam points", which are the frame positions at which the
+ * set of simultaneous events changes (i.e. an event of non-zero
+ * duration starts or ends). These are updated when event is added or
+ * removed.
+ */
+class EventSeries
+{
+public:
+    EventSeries() : m_count(0) { }
+    
+    void add(const Event &p) {
+
+        m_events.insert(p);
+        ++m_count;
+
+        if (p.hasDuration()) {
+            sv_frame_t frame = p.getFrame();
+            sv_frame_t endFrame = p.getFrame() + p.getDuration();
+
+            createSeam(frame);
+            createSeam(endFrame);
+            
+            auto i0 = m_seams.find(frame); // must succeed after createSeam
+            auto i1 = m_seams.find(endFrame); // likewise
+
+            for (auto i = i0; i != i1; ++i) {
+                if (i == m_seams.end()) {
+                    SVCERR << "ERROR: EventSeries::add: "
+                           << "reached end of seam map"
+                           << endl;
+                    break;
+                }
+                i->second.insert(p);
+            }
+        }
+
+#ifdef DEBUG_EVENT_SERIES
+        std::cerr << "after add:" << std::endl;
+        dumpEvents();
+        dumpSeams();
+#endif
+    }
+
+    void remove(const Event &p) {
+
+        // erase first itr that matches p; if there is more than one
+        // p, erase(p) would remove all of them, but we only want to
+        // remove (any) one
+        auto pitr = m_events.find(p);
+        if (pitr == m_events.end()) {
+            return; // we don't know this event
+        } else {
+            m_events.erase(pitr);
+            --m_count;
+        }
+
+        if (p.hasDuration()) {
+            sv_frame_t frame = p.getFrame();
+            sv_frame_t endFrame = p.getFrame() + p.getDuration();
+
+            auto i0 = m_seams.find(frame);
+            auto i1 = m_seams.find(endFrame);
+
+#ifdef DEBUG_EVENT_SERIES
+            // This should be impossible if we found p in m_events above
+            if (i0 == m_seams.end() || i1 == m_seams.end()) {
+                SVCERR << "ERROR: EventSeries::remove: either frame " << frame
+                       << " or endFrame " << endFrame
+                       << " for event not found in seam map: event is "
+                       << p.toXmlString() << endl;
+            }
+#endif
+
+            for (auto i = i0; i != i1; ++i) {
+                if (i == m_seams.end()) {
+                    // This can happen only if we have a negative
+                    // duration, which Event forbids, but we don't
+                    // protect against it in this class, so we'll
+                    // leave this check in
+                    SVCERR << "ERROR: EventSeries::remove: "
+                           << "reached end of seam map"
+                           << endl;
+                    break;
+                }
+                // Can't just erase(p) as that would erase all of
+                // them, if there are several identical ones
+                auto si = i->second.find(p);
+                if (si != i->second.end()) {
+                    i->second.erase(si);
+                }
+            }
+
+            // Shall we "garbage-collect" here? We could be leaving
+            // lots of empty event-sets, or consecutive identical
+            // ones, which are a pure irrelevance that take space and
+            // slow us down. But a lot depends on whether callers ever
+            // really delete anything much.
+        }
+
+#ifdef DEBUG_EVENT_SERIES
+        std::cerr << "after remove:" << std::endl;
+        dumpEvents();
+        dumpSeams();
+#endif
+    }
+
+    bool contains(const Event &p) {
+        return m_events.find(p) != m_events.end();
+    }
+
+    int count() const {
+        return m_count;
+    }
+
+    bool isEmpty() const {
+        return m_count == 0;
+    }
+    
+    void clear() {
+        m_events.clear();
+        m_seams.clear();
+        m_count = 0;
+    }
+
+    /**
+     * Retrieve all events that span the given frame. A event without
+     * duration spans a frame if its own frame is equal to it. A event
+     * with duration spans a frame if its start frame is less than or
+     * equal to it and its end frame (start + duration) is greater
+     * than it.
+     */
+    EventVector getEventsSpanning(sv_frame_t frame) const {
+        EventVector span;
+
+        // first find any zero-duration events
+        auto pitr = m_events.lower_bound(Event(frame, QString()));
+        if (pitr != m_events.end()) {
+            while (pitr->getFrame() == frame) {
+                if (!pitr->hasDuration()) {
+                    span.push_back(*pitr);
+                }
+                ++pitr;
+            }
+        }
+
+        // now any non-zero-duration ones from the seam map
+        auto sitr = m_seams.lower_bound(frame);
+        if (sitr == m_seams.end() || sitr->first > frame) {
+            if (sitr != m_seams.begin()) {
+                --sitr;
+            }                
+        }
+        if (sitr != m_seams.end() && sitr->first <= frame) {
+            for (auto p: sitr->second) {
+                span.push_back(p);
+            }
+        }
+        
+        return span;
+    }
+
+private:
+    int m_count;
+
+    typedef std::multiset<Event> EventMultiset;
+    EventMultiset m_events;
+
+    typedef std::map<sv_frame_t, std::multiset<Event>> FrameEventsMap;
+    FrameEventsMap m_seams;
+
+    /** Create a seam at the given frame, copying from the prior seam
+     *  if there is one. If a seam already exists at the given frame,
+     *  leave it untouched.
+     */
+    void createSeam(sv_frame_t frame) {
+        auto itr = m_seams.lower_bound(frame);
+        if (itr == m_seams.end() || itr->first > frame) {
+            if (itr != m_seams.begin()) {
+                --itr;
+            }
+        }
+        if (itr == m_seams.end()) {
+            m_seams[frame] = {};
+        } else if (itr->first < frame) {
+            m_seams[frame] = itr->second;
+        } else if (itr->first > frame) { // itr must be begin()
+            m_seams[frame] = {};
+        }
+    }
+
+#ifdef DEBUG_EVENT_SERIES
+    void dumpEvents() const {
+        std::cerr << "EVENTS [" << std::endl;
+        for (const auto &p: m_events) {
+            std::cerr << p.toXmlString("  ");
+        }
+        std::cerr << "]" << std::endl;
+    }
+    
+    void dumpSeams() const {
+        std::cerr << "SEAMS [" << std::endl;
+        for (const auto &s: m_seams) {
+            std::cerr << "  " << s.first << " -> {" << std::endl;
+            for (const auto &p: s.second) {
+                std::cerr << p.toXmlString("    ");
+            }
+            std::cerr << "  }" << std::endl;
+        }
+        std::cerr << "]" << std::endl;
+    }
+#endif
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/NoteData.h	Thu Mar 07 15:44:09 2019 +0000
@@ -0,0 +1,57 @@
+/* -*- 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 SV_NOTE_DATA_H
+#define SV_NOTE_DATA_H
+
+#include <vector>
+
+#include "Pitch.h"
+
+/**
+ * Note record used when constructing synthetic events for sonification.
+ */
+struct NoteData
+{
+    NoteData(sv_frame_t _start, sv_frame_t _dur, int _mp, int _vel) :
+        start(_start), duration(_dur), midiPitch(_mp), frequency(0),
+        isMidiPitchQuantized(true), velocity(_vel), channel(0) { };
+            
+    sv_frame_t start;       // audio sample frame
+    sv_frame_t duration;    // in audio sample frames
+    int midiPitch;   // 0-127
+    float frequency; // Hz, to be used if isMidiPitchQuantized false
+    bool isMidiPitchQuantized;
+    int velocity;    // MIDI-style 0-127
+    int channel;     // MIDI 0-15
+
+    float getFrequency() const {
+        if (isMidiPitchQuantized) {
+            return float(Pitch::getFrequencyForPitch(midiPitch));
+        } else {
+            return frequency;
+        }
+    }
+};
+
+typedef std::vector<NoteData> NoteList;
+
+class NoteExportable
+{
+public:
+    virtual NoteList getNotes() const = 0;
+    virtual NoteList getNotesWithin(sv_frame_t startFrame, sv_frame_t endFrame) const = 0;
+};
+
+#endif
--- a/base/Point.h	Thu Mar 07 14:35:57 2019 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,201 +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 2006 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.
-*/
-
-#ifndef SV_POINT_H
-#define SV_POINT_H
-
-#include <QString>
-#include <vector>
-
-#include "BaseTypes.h"
-#include "XmlExportable.h"
-
-//!!! given that these can have size (i.e. duration), maybe Point
-//!!! isn't really an ideal name... perhaps I should go back to dull
-//!!! old Event
-
-class Point
-{
-public:
-    Point(sv_frame_t frame, QString label) :
-        m_haveValue(false), m_haveLevel(false), m_haveReferenceFrame(false),
-        m_value(0.f), m_level(0.f), m_frame(frame),
-        m_duration(0), m_referenceFrame(0), m_label(label) { }
-        
-    Point(sv_frame_t frame, float value, QString label) :
-        m_haveValue(true), m_haveLevel(false), m_haveReferenceFrame(false),
-        m_value(value), m_level(0.f), m_frame(frame),
-        m_duration(0), m_referenceFrame(0), m_label(label) { }
-        
-    Point(sv_frame_t frame, float value, sv_frame_t duration, QString label) :
-        m_haveValue(true), m_haveLevel(false), m_haveReferenceFrame(false),
-        m_value(value), m_level(0.f), m_frame(frame),
-        m_duration(duration), m_referenceFrame(0), m_label(label) { }
-        
-    Point(sv_frame_t frame, float value, sv_frame_t duration,
-          float level, QString label) :
-        m_haveValue(true), m_haveLevel(true), m_haveReferenceFrame(false),
-        m_value(value), m_level(level), m_frame(frame),
-        m_duration(duration), m_referenceFrame(0), m_label(label) { }
-
-    Point(const Point &point) =default;
-    Point &operator=(const Point &point) =default;
-    Point &operator=(Point &&point) =default;
-    
-    sv_frame_t getFrame() const { return m_frame; }
-
-    Point withFrame(sv_frame_t frame) const {
-        Point p(*this);
-        p.m_frame = frame;
-        return p;
-    }
-    
-    bool haveValue() const { return m_haveValue; }
-    float getValue() const { return m_value; }
-    
-    Point withValue(float value) const {
-        Point p(*this);
-        p.m_haveValue = true;
-        p.m_value = value;
-        return p;
-    }
-    
-    bool haveDuration() const { return m_duration != 0; }
-    sv_frame_t getDuration() const { return m_duration; }
-
-    Point withDuration(sv_frame_t duration) const {
-        Point p(*this);
-        p.m_duration = duration;
-        return p;
-    }
-    
-    QString getLabel() const { return m_label; }
-
-    Point withLabel(QString label) const {
-        Point p(*this);
-        p.m_label = label;
-        return p;
-    }
-    
-    bool haveLevel() const { return m_haveLevel; }
-    float getLevel() const { return m_level; }
-    Point withLevel(float level) const {
-        Point p(*this);
-        p.m_haveLevel = true;
-        p.m_level = level;
-        return p;
-    }
-    
-    bool haveReferenceFrame() const { return m_haveReferenceFrame; }
-    sv_frame_t getReferenceFrame() const { return m_referenceFrame; }
-        
-    bool referenceFrameDiffers() const { // from point frame
-        return m_haveReferenceFrame && (m_referenceFrame != m_frame);
-    }
-    
-    Point withReferenceFrame(sv_frame_t frame) const {
-        Point p(*this);
-        p.m_haveReferenceFrame = true;
-        p.m_referenceFrame = frame;
-        return p;
-    }
-
-    bool operator==(const Point &p) const {
-
-        if (m_frame != p.m_frame) return false;
-
-        if (m_haveValue != p.m_haveValue) return false;
-        if (m_haveValue && (m_value != p.m_value)) return false;
-
-        if (m_duration != p.m_duration) return false;
-
-        if (m_haveLevel != p.m_haveLevel) return false;
-        if (m_haveLevel && (m_level != p.m_level)) return false;
-
-        if (m_haveReferenceFrame != p.m_haveReferenceFrame) return false;
-        if (m_haveReferenceFrame &&
-            (m_referenceFrame != p.m_referenceFrame)) return false;
-        
-        if (m_label != p.m_label) return false;
-        
-        return true;
-    }
-
-    bool operator<(const Point &p) const {
-
-        if (m_frame != p.m_frame) return m_frame < p.m_frame;
-
-        // points without a property sort before points with that property
-
-        if (m_haveValue != p.m_haveValue) return !m_haveValue;
-        if (m_haveValue && (m_value != p.m_value)) return m_value < p.m_value;
-        
-        if (m_duration != p.m_duration) return m_duration < p.m_duration;
-        
-        if (m_haveLevel != p.m_haveLevel) return !m_haveLevel;
-        if (m_haveLevel && (m_level != p.m_level)) return m_level < p.m_level;
-
-        if (m_haveReferenceFrame != p.m_haveReferenceFrame) {
-            return !m_haveReferenceFrame;
-        }
-        if (m_haveReferenceFrame && (m_referenceFrame != p.m_referenceFrame)) {
-            return m_referenceFrame < p.m_referenceFrame;
-        }
-        
-        return m_label < p.m_label;
-    }
-
-    void toXml(QTextStream &stream,
-               QString indent = "",
-               QString extraAttributes = "") const {
-
-        stream << indent << QString("<point frame=\"%1\" ").arg(m_frame);
-        if (m_haveValue) stream << QString("value=\"%1\" ").arg(m_value);
-        if (m_duration) stream << QString("duration=\"%1\" ").arg(m_duration);
-        if (m_haveLevel) stream << QString("level=\"%1\" ").arg(m_level);
-        if (m_haveReferenceFrame) stream << QString("referenceFrame=\"%1\" ")
-                                      .arg(m_referenceFrame);
-        stream << QString("label=\"%1\" ")
-            .arg(XmlExportable::encodeEntities(m_label));
-        stream << extraAttributes << ">\n";
-    }
-
-    QString toXmlString(QString indent = "",
-                        QString extraAttributes = "") const {
-        QString s;
-        QTextStream out(&s);
-        toXml(out, indent, extraAttributes);
-        out.flush();
-        return s;
-    }
-    
-private:
-    // The order of fields here is chosen to minimise overall size of struct.
-    // We potentially store very many of these objects.
-    // If you change something, check what difference it makes to packing.
-    bool m_haveValue : 1;
-    bool m_haveLevel : 1;
-    bool m_haveReferenceFrame : 1;
-    float m_value;
-    float m_level;
-    sv_frame_t m_frame;
-    sv_frame_t m_duration;
-    sv_frame_t m_referenceFrame;
-    QString m_label;
-};
-
-typedef std::vector<Point> PointVector;
-
-#endif
--- a/base/PointSeries.h	Thu Mar 07 14:35:57 2019 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,228 +0,0 @@
-/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
-
-/*
-    Sonic Visualiser
-    An audio file viewer and annotation editor.
-    Centre for Digital Music, Queen Mary, University of London.
-    
-    This program is free software; you can redistribute it and/or
-    modify it under the terms of the GNU General Public License as
-    published by the Free Software Foundation; either version 2 of the
-    License, or (at your option) any later version.  See the file
-    COPYING included with this distribution for more information.
-*/
-
-#ifndef SV_POINT_SERIES_H
-#define SV_POINT_SERIES_H
-
-#include "Point.h"
-
-#include <set>
-
-//#define DEBUG_POINT_SERIES 1
-
-class PointSeries
-{
-public:
-    PointSeries() : m_count(0) { }
-    
-    void add(const Point &p) {
-
-        m_points.insert(p);
-        ++m_count;
-
-        if (p.haveDuration()) {
-            sv_frame_t frame = p.getFrame();
-            sv_frame_t endFrame = p.getFrame() + p.getDuration();
-
-            createSeam(frame);
-            createSeam(endFrame);
-            
-            auto i0 = m_seams.find(frame); // must succeed after createSeam
-            auto i1 = m_seams.find(endFrame); // likewise
-
-            for (auto i = i0; i != i1; ++i) {
-                if (i == m_seams.end()) {
-                    SVCERR << "ERROR: PointSeries::add: "
-                           << "reached end of seam map"
-                           << endl;
-                    break;
-                }
-                i->second.insert(p);
-            }
-        }
-
-#ifdef DEBUG_POINT_SERIES
-        std::cerr << "after add:" << std::endl;
-        dumpPoints();
-        dumpSeams();
-#endif
-    }
-
-    void remove(const Point &p) {
-
-        // erase first itr that matches p; if there is more than one
-        // p, erase(p) would remove all of them, but we only want to
-        // remove (any) one
-        auto pitr = m_points.find(p);
-        if (pitr == m_points.end()) {
-            return; // we don't know this point
-        } else {
-            m_points.erase(pitr);
-            --m_count;
-        }
-
-        if (p.haveDuration()) {
-            sv_frame_t frame = p.getFrame();
-            sv_frame_t endFrame = p.getFrame() + p.getDuration();
-
-            auto i0 = m_seams.find(frame);
-            auto i1 = m_seams.find(endFrame);
-
-#ifdef DEBUG_POINT_SERIES
-            // This should be impossible if we found p in m_points above
-            if (i0 == m_seams.end() || i1 == m_seams.end()) {
-                SVCERR << "ERROR: PointSeries::remove: either frame " << frame
-                       << " or endFrame " << endFrame
-                       << " for point not found in seam map: point is "
-                       << p.toXmlString() << endl;
-            }
-#endif
-
-            for (auto i = i0; i != i1; ++i) {
-                if (i == m_seams.end()) {
-                    SVCERR << "ERROR: PointSeries::remove: "
-                           << "reached end of seam map"
-                           << endl;
-                    break;
-                }
-                // Can't just erase(p) as that would erase all of
-                // them, if there are several identical ones
-                auto si = i->second.find(p);
-                if (si != i->second.end()) {
-                    i->second.erase(si);
-                }
-            }
-
-            // Shall we "garbage-collect" here? We could be leaving
-            // lots of empty point-sets, or consecutive identical
-            // ones, which are a pure irrelevance that take space and
-            // slow us down. But a lot depends on whether callers ever
-            // really delete anything much.
-        }
-
-#ifdef DEBUG_POINT_SERIES
-        std::cerr << "after remove:" << std::endl;
-        dumpPoints();
-        dumpSeams();
-#endif
-    }
-
-    bool contains(const Point &p) {
-        return m_points.find(p) != m_points.end();
-    }
-
-    int count() const {
-        return m_count;
-    }
-
-    bool isEmpty() const {
-        return m_count == 0;
-    }
-    
-    void clear() {
-        m_points.clear();
-        m_seams.clear();
-        m_count = 0;
-    }
-
-    /**
-     * Retrieve all points that span the given frame. A point without
-     * duration spans a frame if its own frame is equal to it. A point
-     * with duration spans a frame if its start frame is less than or
-     * equal to it and its end frame (start + duration) is greater
-     * than it.
-     */
-    PointVector getPointsSpanning(sv_frame_t frame) const {
-        PointVector span;
-
-        // first find any zero-duration points
-        auto pitr = m_points.lower_bound(Point(frame, QString()));
-        if (pitr != m_points.end()) {
-            while (pitr->getFrame() == frame) {
-                if (!pitr->haveDuration()) {
-                    span.push_back(*pitr);
-                }
-                ++pitr;
-            }
-        }
-
-        // now any non-zero-duration ones from the seam map
-        auto sitr = m_seams.lower_bound(frame);
-        if (sitr == m_seams.end() || sitr->first > frame) {
-            if (sitr != m_seams.begin()) {
-                --sitr;
-            }                
-        }
-        if (sitr != m_seams.end() && sitr->first <= frame) {
-            for (auto p: sitr->second) {
-                span.push_back(p);
-            }
-        }
-        
-        return span;
-    }
-
-private:
-    int m_count;
-
-    typedef std::multiset<Point> PointMultiset;
-    PointMultiset m_points;
-
-    typedef std::map<sv_frame_t, std::multiset<Point>> FramePointsMap;
-    FramePointsMap m_seams;
-
-    /** Create a seam at the given frame, copying from the prior seam
-     *  if there is one. If a seam already exists at the given frame,
-     *  leave it untouched.
-     */
-    void createSeam(sv_frame_t frame) {
-        auto itr = m_seams.lower_bound(frame);
-        if (itr == m_seams.end() || itr->first > frame) {
-            if (itr != m_seams.begin()) {
-                --itr;
-            }
-        }
-        if (itr == m_seams.end()) {
-            m_seams[frame] = {};
-        } else if (itr->first < frame) {
-            m_seams[frame] = itr->second;
-        } else if (itr->first > frame) { // itr must be begin()
-            m_seams[frame] = {};
-        }
-    }
-
-#ifdef DEBUG_POINT_SERIES
-    void dumpPoints() const {
-        std::cerr << "POINTS [" << std::endl;
-        for (const auto &p: m_points) {
-            std::cerr << p.toXmlString("  ");
-        }
-        std::cerr << "]" << std::endl;
-    }
-    
-    void dumpSeams() const {
-        std::cerr << "SEAMS [" << std::endl;
-        for (const auto &s: m_seams) {
-            std::cerr << "  " << s.first << " -> {" << std::endl;
-            for (const auto &p: s.second) {
-                std::cerr << p.toXmlString("    ");
-            }
-            std::cerr << "  }" << std::endl;
-        }
-        std::cerr << "]" << std::endl;
-    }
-#endif
-};
-
-#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/test/TestEventSeries.h	Thu Mar 07 15:44:09 2019 +0000
@@ -0,0 +1,221 @@
+/* -*- 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_EVENT_SERIES_H
+#define TEST_EVENT_SERIES_H
+
+#include "../EventSeries.h"
+
+#include <QObject>
+#include <QtTest>
+
+#include <iostream>
+
+using namespace std;
+
+class TestEventSeries : public QObject
+{
+    Q_OBJECT
+
+private slots:
+    void empty() {
+
+        EventSeries s;
+        QCOMPARE(s.isEmpty(), true);
+        QCOMPARE(s.count(), 0);
+
+        Event p(10, QString());
+        QCOMPARE(s.contains(p), false);
+        QCOMPARE(s.getEventsSpanning(400), EventVector());
+    }
+
+    void singleEvent() {
+
+        EventSeries s;
+        Event p(10, QString());
+        s.add(p);
+        QCOMPARE(s.isEmpty(), false);
+        QCOMPARE(s.count(), 1);
+        QCOMPARE(s.contains(p), true);
+
+        s.remove(p);
+        QCOMPARE(s.isEmpty(), true);
+        QCOMPARE(s.contains(p), false);
+    }
+
+    void singleEventSpan() {
+
+        EventSeries s;
+        Event p(10, QString());
+        s.add(p);
+        EventVector span;
+        span.push_back(p);
+        QCOMPARE(s.getEventsSpanning(10), span);
+        QCOMPARE(s.getEventsSpanning(11), EventVector());
+        QCOMPARE(s.getEventsSpanning(9), EventVector());
+    }
+
+    void singleEventWithDurationSpan() {
+
+        EventSeries s;
+        Event p(10, 1.0, 20, QString());
+        s.add(p);
+        EventVector span;
+        span.push_back(p);
+        QCOMPARE(s.getEventsSpanning(10), span);
+        QCOMPARE(s.getEventsSpanning(11), span);
+        QCOMPARE(s.getEventsSpanning(29), span);
+        QCOMPARE(s.getEventsSpanning(30), EventVector());
+        QCOMPARE(s.getEventsSpanning(9), EventVector());
+    }
+
+    void identicalEventsSpan() {
+
+        EventSeries s;
+        Event p(10, QString());
+        s.add(p);
+        s.add(p);
+
+        EventVector span;
+        span.push_back(p);
+        span.push_back(p);
+        QCOMPARE(s.getEventsSpanning(10), span);
+        QCOMPARE(s.getEventsSpanning(11), EventVector());
+        QCOMPARE(s.getEventsSpanning(9), EventVector());
+
+        s.remove(p);
+        span.clear();
+        span.push_back(p);
+        QCOMPARE(s.getEventsSpanning(10), span);
+        QCOMPARE(s.getEventsSpanning(11), EventVector());
+        QCOMPARE(s.getEventsSpanning(9), EventVector());
+    }
+    
+    void identicalEventsWithDurationSpan() {
+
+        EventSeries s;
+        Event p(10, 1.0, 20, QString());
+        s.add(p);
+        s.add(p);
+        EventVector span;
+        span.push_back(p);
+        span.push_back(p);
+        QCOMPARE(s.getEventsSpanning(10), span);
+        QCOMPARE(s.getEventsSpanning(11), span);
+        QCOMPARE(s.getEventsSpanning(29), span);
+        QCOMPARE(s.getEventsSpanning(30), EventVector());
+        QCOMPARE(s.getEventsSpanning(9), EventVector());
+
+        s.remove(p);
+        span.clear();
+        span.push_back(p);
+        QCOMPARE(s.getEventsSpanning(10), span);
+        QCOMPARE(s.getEventsSpanning(11), span);
+        QCOMPARE(s.getEventsSpanning(29), span);
+        QCOMPARE(s.getEventsSpanning(30), EventVector());
+        QCOMPARE(s.getEventsSpanning(9), EventVector());
+    }
+
+    void multipleEventsSpan() {
+
+        EventSeries s;
+        Event a(10, QString("a"));
+        Event b(11, QString("b"));
+        Event c(40, QString("c"));
+        s.add(c);
+        s.add(a);
+        s.add(b);
+        s.remove(a);
+        s.add(a);
+        s.add(c);
+        s.remove(c);
+        QCOMPARE(s.count(), 3);
+        EventVector span;
+        span.push_back(a);
+        QCOMPARE(s.getEventsSpanning(10), span);
+        span.clear();
+        span.push_back(c);
+        QCOMPARE(s.getEventsSpanning(40), span);
+        QCOMPARE(s.getEventsSpanning(9), EventVector());
+    }
+
+    void disjointEventsWithDurationSpan() {
+
+        EventSeries s;
+        Event a(10, 1.0f, 20, QString("a"));
+        Event b(100, 1.2f, 30, QString("b"));
+        s.add(a);
+        s.add(b);
+        QCOMPARE(s.getEventsSpanning(0), EventVector());
+        QCOMPARE(s.getEventsSpanning(10), EventVector({ a }));
+        QCOMPARE(s.getEventsSpanning(15), EventVector({ a }));
+        QCOMPARE(s.getEventsSpanning(30), EventVector());
+        QCOMPARE(s.getEventsSpanning(99), EventVector());
+        QCOMPARE(s.getEventsSpanning(100), EventVector({ b }));
+        QCOMPARE(s.getEventsSpanning(120), EventVector({ b }));
+        QCOMPARE(s.getEventsSpanning(130), EventVector());
+    }
+    
+    void overlappingEventsWithAndWithoutDurationSpan() {
+
+        EventSeries s;
+        Event p(20, QString("p"));
+        Event a(10, 1.0, 20, QString("a"));
+        s.add(p);
+        s.add(a);
+        EventVector span;
+        span.push_back(a);
+        QCOMPARE(s.getEventsSpanning(15), span);
+        QCOMPARE(s.getEventsSpanning(25), span);
+        span.clear();
+        span.push_back(p);
+        span.push_back(a);
+        QCOMPARE(s.getEventsSpanning(20), span);
+    }
+
+    void overlappingEventsWithDurationSpan() {
+
+        EventSeries s;
+        Event a(20, 1.0, 10, QString("a"));
+        Event b(10, 1.0, 20, QString("b"));
+        Event c(10, 1.0, 40, QString("c"));
+        s.add(a);
+        s.add(b);
+        s.add(c);
+        QCOMPARE(s.getEventsSpanning(10), EventVector({ b, c }));
+        QCOMPARE(s.getEventsSpanning(20), EventVector({ b, c, a }));
+        QCOMPARE(s.getEventsSpanning(25), EventVector({ b, c, a }));
+        QCOMPARE(s.getEventsSpanning(30), EventVector({ c }));
+        QCOMPARE(s.getEventsSpanning(40), EventVector({ c }));
+        QCOMPARE(s.getEventsSpanning(50), EventVector());
+    }
+
+    void eventPatternSpan() {
+
+        EventSeries s;
+        Event a(0, 1.0, 18, QString("a"));
+        Event b(3, 2.0, 6, QString("b"));
+        Event c(5, 3.0, 2, QString("c"));
+        Event d(6, 4.0, 10, QString("d"));
+        Event e(14, 5.0, 3, QString("e"));
+        s.add(b);
+        s.add(c);
+        s.add(d);
+        s.add(a);
+        s.add(e);
+        QCOMPARE(s.getEventsSpanning(8), EventVector({ a, b, d }));
+    }
+};
+
+#endif
--- a/base/test/TestPointSeries.h	Thu Mar 07 14:35:57 2019 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,221 +0,0 @@
-/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
-
-/*
-    Sonic Visualiser
-    An audio file viewer and annotation editor.
-    Centre for Digital Music, Queen Mary, University of London.
-    
-    This program is free software; you can redistribute it and/or
-    modify it under the terms of the GNU General Public License as
-    published by the Free Software Foundation; either version 2 of the
-    License, or (at your option) any later version.  See the file
-    COPYING included with this distribution for more information.
-*/
-
-#ifndef TEST_POINT_SERIES_H
-#define TEST_POINT_SERIES_H
-
-#include "../PointSeries.h"
-
-#include <QObject>
-#include <QtTest>
-
-#include <iostream>
-
-using namespace std;
-
-class TestPointSeries : public QObject
-{
-    Q_OBJECT
-
-private slots:
-    void empty() {
-
-        PointSeries s;
-        QCOMPARE(s.isEmpty(), true);
-        QCOMPARE(s.count(), 0);
-
-        Point p(10, QString());
-        QCOMPARE(s.contains(p), false);
-        QCOMPARE(s.getPointsSpanning(400), PointVector());
-    }
-
-    void singlePoint() {
-
-        PointSeries s;
-        Point p(10, QString());
-        s.add(p);
-        QCOMPARE(s.isEmpty(), false);
-        QCOMPARE(s.count(), 1);
-        QCOMPARE(s.contains(p), true);
-
-        s.remove(p);
-        QCOMPARE(s.isEmpty(), true);
-        QCOMPARE(s.contains(p), false);
-    }
-
-    void singlePointSpan() {
-
-        PointSeries s;
-        Point p(10, QString());
-        s.add(p);
-        PointVector span;
-        span.push_back(p);
-        QCOMPARE(s.getPointsSpanning(10), span);
-        QCOMPARE(s.getPointsSpanning(11), PointVector());
-        QCOMPARE(s.getPointsSpanning(9), PointVector());
-    }
-
-    void singlePointWithDurationSpan() {
-
-        PointSeries s;
-        Point p(10, 1.0, 20, QString());
-        s.add(p);
-        PointVector span;
-        span.push_back(p);
-        QCOMPARE(s.getPointsSpanning(10), span);
-        QCOMPARE(s.getPointsSpanning(11), span);
-        QCOMPARE(s.getPointsSpanning(29), span);
-        QCOMPARE(s.getPointsSpanning(30), PointVector());
-        QCOMPARE(s.getPointsSpanning(9), PointVector());
-    }
-
-    void identicalPointsSpan() {
-
-        PointSeries s;
-        Point p(10, QString());
-        s.add(p);
-        s.add(p);
-
-        PointVector span;
-        span.push_back(p);
-        span.push_back(p);
-        QCOMPARE(s.getPointsSpanning(10), span);
-        QCOMPARE(s.getPointsSpanning(11), PointVector());
-        QCOMPARE(s.getPointsSpanning(9), PointVector());
-
-        s.remove(p);
-        span.clear();
-        span.push_back(p);
-        QCOMPARE(s.getPointsSpanning(10), span);
-        QCOMPARE(s.getPointsSpanning(11), PointVector());
-        QCOMPARE(s.getPointsSpanning(9), PointVector());
-    }
-    
-    void identicalPointsWithDurationSpan() {
-
-        PointSeries s;
-        Point p(10, 1.0, 20, QString());
-        s.add(p);
-        s.add(p);
-        PointVector span;
-        span.push_back(p);
-        span.push_back(p);
-        QCOMPARE(s.getPointsSpanning(10), span);
-        QCOMPARE(s.getPointsSpanning(11), span);
-        QCOMPARE(s.getPointsSpanning(29), span);
-        QCOMPARE(s.getPointsSpanning(30), PointVector());
-        QCOMPARE(s.getPointsSpanning(9), PointVector());
-
-        s.remove(p);
-        span.clear();
-        span.push_back(p);
-        QCOMPARE(s.getPointsSpanning(10), span);
-        QCOMPARE(s.getPointsSpanning(11), span);
-        QCOMPARE(s.getPointsSpanning(29), span);
-        QCOMPARE(s.getPointsSpanning(30), PointVector());
-        QCOMPARE(s.getPointsSpanning(9), PointVector());
-    }
-
-    void multiplePointsSpan() {
-
-        PointSeries s;
-        Point a(10, QString("a"));
-        Point b(11, QString("b"));
-        Point c(40, QString("c"));
-        s.add(c);
-        s.add(a);
-        s.add(b);
-        s.remove(a);
-        s.add(a);
-        s.add(c);
-        s.remove(c);
-        QCOMPARE(s.count(), 3);
-        PointVector span;
-        span.push_back(a);
-        QCOMPARE(s.getPointsSpanning(10), span);
-        span.clear();
-        span.push_back(c);
-        QCOMPARE(s.getPointsSpanning(40), span);
-        QCOMPARE(s.getPointsSpanning(9), PointVector());
-    }
-
-    void disjointPointsWithDurationSpan() {
-
-        PointSeries s;
-        Point a(10, 1.0f, 20, QString("a"));
-        Point b(100, 1.2f, 30, QString("b"));
-        s.add(a);
-        s.add(b);
-        QCOMPARE(s.getPointsSpanning(0), PointVector());
-        QCOMPARE(s.getPointsSpanning(10), PointVector({ a }));
-        QCOMPARE(s.getPointsSpanning(15), PointVector({ a }));
-        QCOMPARE(s.getPointsSpanning(30), PointVector());
-        QCOMPARE(s.getPointsSpanning(99), PointVector());
-        QCOMPARE(s.getPointsSpanning(100), PointVector({ b }));
-        QCOMPARE(s.getPointsSpanning(120), PointVector({ b }));
-        QCOMPARE(s.getPointsSpanning(130), PointVector());
-    }
-    
-    void overlappingPointsWithAndWithoutDurationSpan() {
-
-        PointSeries s;
-        Point p(20, QString("p"));
-        Point a(10, 1.0, 20, QString("a"));
-        s.add(p);
-        s.add(a);
-        PointVector span;
-        span.push_back(a);
-        QCOMPARE(s.getPointsSpanning(15), span);
-        QCOMPARE(s.getPointsSpanning(25), span);
-        span.clear();
-        span.push_back(p);
-        span.push_back(a);
-        QCOMPARE(s.getPointsSpanning(20), span);
-    }
-
-    void overlappingPointsWithDurationSpan() {
-
-        PointSeries s;
-        Point a(20, 1.0, 10, QString("a"));
-        Point b(10, 1.0, 20, QString("b"));
-        Point c(10, 1.0, 40, QString("c"));
-        s.add(a);
-        s.add(b);
-        s.add(c);
-        QCOMPARE(s.getPointsSpanning(10), PointVector({ b, c }));
-        QCOMPARE(s.getPointsSpanning(20), PointVector({ b, c, a }));
-        QCOMPARE(s.getPointsSpanning(25), PointVector({ b, c, a }));
-        QCOMPARE(s.getPointsSpanning(30), PointVector({ c }));
-        QCOMPARE(s.getPointsSpanning(40), PointVector({ c }));
-        QCOMPARE(s.getPointsSpanning(50), PointVector());
-    }
-
-    void pointPatternSpan() {
-
-        PointSeries s;
-        Point a(0, 1.0, 18, QString("a"));
-        Point b(3, 2.0, 6, QString("b"));
-        Point c(5, 3.0, 2, QString("c"));
-        Point d(6, 4.0, 10, QString("d"));
-        Point e(14, 5.0, 3, QString("e"));
-        s.add(b);
-        s.add(c);
-        s.add(d);
-        s.add(a);
-        s.add(e);
-        QCOMPARE(s.getPointsSpanning(8), PointVector({ a, b, d }));
-    }
-};
-
-#endif
--- a/base/test/files.pri	Thu Mar 07 14:35:57 2019 +0000
+++ b/base/test/files.pri	Thu Mar 07 15:44:09 2019 +0000
@@ -4,7 +4,7 @@
 	     TestMovingMedian.h \
 	     TestOurRealTime.h \
 	     TestPitch.h \
-	     TestPointSeries.h \
+	     TestEventSeries.h \
 	     TestRangeMapper.h \
 	     TestScaleTickIntervals.h \
 	     TestStringBits.h \
--- a/base/test/svcore-base-test.cpp	Thu Mar 07 14:35:57 2019 +0000
+++ b/base/test/svcore-base-test.cpp	Thu Mar 07 15:44:09 2019 +0000
@@ -20,7 +20,7 @@
 #include "TestVampRealTime.h"
 #include "TestColumnOp.h"
 #include "TestMovingMedian.h"
-#include "TestPointSeries.h"
+#include "TestEventSeries.h"
 
 #include "system/Init.h"
 
@@ -86,7 +86,7 @@
         else ++bad;
     }
     {
-        TestPointSeries t;
+        TestEventSeries t;
         if (QTest::qExec(&t, argc, argv) == 0) ++good;
         else ++bad;
     }
--- a/data/fileio/MIDIFileWriter.cpp	Thu Mar 07 14:35:57 2019 +0000
+++ b/data/fileio/MIDIFileWriter.cpp	Thu Mar 07 15:44:09 2019 +0000
@@ -23,8 +23,8 @@
 #include "MIDIFileWriter.h"
 
 #include "data/midi/MIDIEvent.h"
-#include "model/NoteData.h"
 
+#include "base/NoteData.h"
 #include "base/Pitch.h"
 
 #include <QCoreApplication>
--- a/data/model/FlexiNoteModel.h	Thu Mar 07 14:35:57 2019 +0000
+++ b/data/model/FlexiNoteModel.h	Thu Mar 07 15:44:09 2019 +0000
@@ -17,7 +17,7 @@
 #define SV_FLEXINOTE_MODEL_H
 
 #include "IntervalModel.h"
-#include "NoteData.h"
+#include "base/NoteData.h"
 #include "base/RealTime.h"
 #include "base/Pitch.h"
 #include "base/PlayParameterRepository.h"
--- a/data/model/NoteData.h	Thu Mar 07 14:35:57 2019 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
-/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
-
-/*
-    Sonic Visualiser
-    An audio file viewer and annotation editor.
-    Centre for Digital Music, Queen Mary, University of London.
-    
-    This program is free software; you can redistribute it and/or
-    modify it under the terms of the GNU General Public License as
-    published by the Free Software Foundation; either version 2 of the
-    License, or (at your option) any later version.  See the file
-    COPYING included with this distribution for more information.
-*/
-
-#ifndef SV_NOTE_DATA_H
-#define SV_NOTE_DATA_H
-
-#include <vector>
-
-#include "base/Pitch.h"
-
-struct NoteData
-{
-    NoteData(sv_frame_t _start, sv_frame_t _dur, int _mp, int _vel) :
-        start(_start), duration(_dur), midiPitch(_mp), frequency(0),
-        isMidiPitchQuantized(true), velocity(_vel), channel(0) { };
-            
-    sv_frame_t start;       // audio sample frame
-    sv_frame_t duration;    // in audio sample frames
-    int midiPitch;   // 0-127
-    float frequency; // Hz, to be used if isMidiPitchQuantized false
-    bool isMidiPitchQuantized;
-    int velocity;    // MIDI-style 0-127
-    int channel;     // MIDI 0-15
-
-    float getFrequency() const {
-        if (isMidiPitchQuantized) {
-            return float(Pitch::getFrequencyForPitch(midiPitch));
-        } else {
-            return frequency;
-        }
-    }
-};
-
-typedef std::vector<NoteData> NoteList;
-
-class NoteExportable
-{
-public:
-    virtual NoteList getNotes() const = 0;
-    virtual NoteList getNotesWithin(sv_frame_t startFrame, sv_frame_t endFrame) const = 0;
-};
-
-#endif
--- a/data/model/NoteModel.h	Thu Mar 07 14:35:57 2019 +0000
+++ b/data/model/NoteModel.h	Thu Mar 07 15:44:09 2019 +0000
@@ -17,7 +17,7 @@
 #define SV_NOTE_MODEL_H
 
 #include "IntervalModel.h"
-#include "NoteData.h"
+#include "base/NoteData.h"
 #include "base/RealTime.h"
 #include "base/PlayParameterRepository.h"
 #include "base/Pitch.h"
--- a/data/model/SparseOneDimensionalModel.h	Thu Mar 07 14:35:57 2019 +0000
+++ b/data/model/SparseOneDimensionalModel.h	Thu Mar 07 15:44:09 2019 +0000
@@ -17,7 +17,7 @@
 #define SV_SPARSE_ONE_DIMENSIONAL_MODEL_H
 
 #include "SparseModel.h"
-#include "NoteData.h"
+#include "base/NoteData.h"
 #include "base/PlayParameterRepository.h"
 #include "base/RealTime.h"
 
--- a/files.pri	Thu Mar 07 14:35:57 2019 +0000
+++ b/files.pri	Thu Mar 07 15:44:09 2019 +0000
@@ -7,16 +7,18 @@
            base/ColumnOp.h \
            base/Command.h \
            base/Debug.h \
+           base/Event.h \
+           base/EventSeries.h \
            base/Exceptions.h \
            base/HelperExecPath.h \
            base/HitCount.h \
            base/LogRange.h \
            base/MagnitudeRange.h \
+           base/NoteData.h \
            base/Pitch.h \
            base/Playable.h \
            base/PlayParameterRepository.h \
            base/PlayParameters.h \
-           base/Point.h \
            base/Preferences.h \
            base/Profiler.h \
            base/ProgressPrinter.h \