# HG changeset patch # User Chris Cannam # Date 1558083763 -3600 # Node ID 978c143c767fbe5951ae7d8667aa4ca35137c3bf # Parent ab4fd193262b93410c0fe09175b4a1df43ed5753# Parent 54a954ee7529d9f419ba00d0d7e2324493b98683 Merge from branch single-point diff -r ab4fd193262b -r 978c143c767f base/BaseTypes.h --- a/base/BaseTypes.h Thu May 16 12:54:58 2019 +0100 +++ b/base/BaseTypes.h Fri May 17 10:02:43 2019 +0100 @@ -55,5 +55,11 @@ typedef std::vector, breakfastquay::StlAllocator>> complexvec_t; +typedef uint64_t sv_id_t; + +enum { + ID_NOTHING = 0 +}; + #endif diff -r ab4fd193262b -r 978c143c767f base/Clipboard.cpp --- a/base/Clipboard.cpp Thu May 16 12:54:58 2019 +0100 +++ b/base/Clipboard.cpp Fri May 17 10:02:43 2019 +0100 @@ -15,235 +15,6 @@ #include "Clipboard.h" -Clipboard::Point::Point(sv_frame_t frame, QString label) : - m_haveFrame(true), - m_frame(frame), - m_haveValue(false), - m_value(0), - m_haveDuration(false), - m_duration(0), - m_haveLabel(true), - m_label(label), - m_haveLevel(false), - m_level(0.f), - m_haveReferenceFrame(false), - m_referenceFrame(frame) -{ -} - -Clipboard::Point::Point(sv_frame_t frame, float value, QString label) : - m_haveFrame(true), - m_frame(frame), - m_haveValue(true), - m_value(value), - m_haveDuration(false), - m_duration(0), - m_haveLabel(true), - m_label(label), - m_haveLevel(false), - m_level(0.f), - m_haveReferenceFrame(false), - m_referenceFrame(frame) -{ -} - -Clipboard::Point::Point(sv_frame_t frame, float value, sv_frame_t duration, QString label) : - m_haveFrame(true), - m_frame(frame), - m_haveValue(true), - m_value(value), - m_haveDuration(true), - m_duration(duration), - m_haveLabel(true), - m_label(label), - m_haveLevel(false), - m_level(0.f), - m_haveReferenceFrame(false), - m_referenceFrame(frame) -{ -} - -Clipboard::Point::Point(sv_frame_t frame, float value, sv_frame_t duration, float level, QString label) : - m_haveFrame(true), - m_frame(frame), - m_haveValue(true), - m_value(value), - m_haveDuration(true), - m_duration(duration), - m_haveLabel(true), - m_label(label), - m_haveLevel(true), - m_level(level), - m_haveReferenceFrame(false), - m_referenceFrame(frame) -{ -} - -Clipboard::Point::Point(const Point &point) : - m_haveFrame(point.m_haveFrame), - m_frame(point.m_frame), - m_haveValue(point.m_haveValue), - m_value(point.m_value), - m_haveDuration(point.m_haveDuration), - m_duration(point.m_duration), - m_haveLabel(point.m_haveLabel), - m_label(point.m_label), - m_haveLevel(point.m_haveLevel), - m_level(point.m_level), - m_haveReferenceFrame(point.m_haveReferenceFrame), - m_referenceFrame(point.m_referenceFrame) -{ -} - -Clipboard::Point & -Clipboard::Point::operator=(const Point &point) -{ - if (this == &point) return *this; - m_haveFrame = point.m_haveFrame; - m_frame = point.m_frame; - m_haveValue = point.m_haveValue; - m_value = point.m_value; - m_haveDuration = point.m_haveDuration; - m_duration = point.m_duration; - m_haveLabel = point.m_haveLabel; - m_label = point.m_label; - m_haveLevel = point.m_haveLevel; - m_level = point.m_level; - m_haveReferenceFrame = point.m_haveReferenceFrame; - m_referenceFrame = point.m_referenceFrame; - return *this; -} - -bool -Clipboard::Point::haveFrame() const -{ - return m_haveFrame; -} - -sv_frame_t -Clipboard::Point::getFrame() const -{ - return m_frame; -} - -Clipboard::Point -Clipboard::Point::withFrame(sv_frame_t frame) const -{ - Point p(*this); - p.m_haveFrame = true; - p.m_frame = frame; - return p; -} - -bool -Clipboard::Point::haveValue() const -{ - return m_haveValue; -} - -float -Clipboard::Point::getValue() const -{ - return m_value; -} - -Clipboard::Point -Clipboard::Point::withValue(float value) const -{ - Point p(*this); - p.m_haveValue = true; - p.m_value = value; - return p; -} - -bool -Clipboard::Point::haveDuration() const -{ - return m_haveDuration; -} - -sv_frame_t -Clipboard::Point::getDuration() const -{ - return m_duration; -} - -Clipboard::Point -Clipboard::Point::withDuration(sv_frame_t duration) const -{ - Point p(*this); - p.m_haveDuration = true; - p.m_duration = duration; - return p; -} - -bool -Clipboard::Point::haveLabel() const -{ - return m_haveLabel; -} - -QString -Clipboard::Point::getLabel() const -{ - return m_label; -} - -Clipboard::Point -Clipboard::Point::withLabel(QString label) const -{ - Point p(*this); - p.m_haveLabel = true; - p.m_label = label; - return p; -} - -bool -Clipboard::Point::haveLevel() const -{ - return m_haveLevel; -} - -float -Clipboard::Point::getLevel() const -{ - return m_level; -} - -Clipboard::Point -Clipboard::Point::withLevel(float level) const -{ - Point p(*this); - p.m_haveLevel = true; - p.m_level = level; - return p; -} - -bool -Clipboard::Point::haveReferenceFrame() const -{ - return m_haveReferenceFrame; -} - -bool -Clipboard::Point::referenceFrameDiffers() const -{ - return m_haveReferenceFrame && (m_referenceFrame != m_frame); -} - -sv_frame_t -Clipboard::Point::getReferenceFrame() const -{ - return m_referenceFrame; -} - -void -Clipboard::Point::setReferenceFrame(sv_frame_t f) -{ - m_haveReferenceFrame = true; - m_referenceFrame = f; -} - Clipboard::Clipboard() { } Clipboard::~Clipboard() { } @@ -259,20 +30,20 @@ return m_points.empty(); } -const Clipboard::PointList & +const EventVector & Clipboard::getPoints() const { return m_points; } void -Clipboard::setPoints(const PointList &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); } @@ -280,9 +51,9 @@ bool Clipboard::haveReferenceFrames() const { - for (PointList::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; } @@ -290,7 +61,7 @@ bool Clipboard::referenceFramesDiffer() const { - for (PointList::const_iterator i = m_points.begin(); + for (EventVector::const_iterator i = m_points.begin(); i != m_points.end(); ++i) { if (i->referenceFrameDiffers()) return true; } diff -r ab4fd193262b -r 978c143c767f base/Clipboard.h --- a/base/Clipboard.h Thu May 16 12:54:58 2019 +0100 +++ b/base/Clipboard.h Fri May 17 10:02:43 2019 +0100 @@ -16,81 +16,27 @@ #ifndef SV_CLIPBOARD_H #define SV_CLIPBOARD_H -#include #include -#include "BaseTypes.h" +#include "Event.h" class Clipboard { public: - class Point - { - public: - Point(sv_frame_t frame, QString label); - Point(sv_frame_t frame, float value, QString label); - Point(sv_frame_t frame, float value, sv_frame_t duration, QString label); - Point(sv_frame_t frame, float value, sv_frame_t duration, float level, QString label); - Point(const Point &point); - Point &operator=(const Point &point); - - bool haveFrame() const; - sv_frame_t getFrame() const; - Point withFrame(sv_frame_t frame) const; - - bool haveValue() const; - float getValue() const; - Point withValue(float value) const; - - bool haveDuration() const; - sv_frame_t getDuration() const; - Point withDuration(sv_frame_t duration) const; - - bool haveLabel() const; - QString getLabel() const; - Point withLabel(QString label) const; - - bool haveLevel() const; - float getLevel() const; - Point withLevel(float level) const; - - bool haveReferenceFrame() const; - bool referenceFrameDiffers() const; // from point frame - - sv_frame_t getReferenceFrame() const; - void setReferenceFrame(sv_frame_t); - - private: - bool m_haveFrame; - sv_frame_t m_frame; - bool m_haveValue; - float m_value; - bool m_haveDuration; - sv_frame_t m_duration; - bool m_haveLabel; - QString m_label; - bool m_haveLevel; - float m_level; - bool m_haveReferenceFrame; - sv_frame_t m_referenceFrame; - }; - Clipboard(); ~Clipboard(); - typedef std::vector PointList; - void clear(); bool empty() const; - const PointList &getPoints() const; - void setPoints(const PointList &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: - PointList m_points; + EventVector m_points; }; #endif diff -r ab4fd193262b -r 978c143c767f base/Event.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/base/Event.h Fri May 17 10:02:43 2019 +0100 @@ -0,0 +1,414 @@ +/* -*- 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 "DataExportOptions.h" + +#include +#include + +#include + +/** + * An immutable(-ish) 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() : + m_haveValue(false), m_haveLevel(false), + m_haveDuration(false), m_haveReferenceFrame(false), + m_value(0.f), m_level(0.f), m_frame(0), + m_duration(0), m_referenceFrame(0), m_label() { } + + Event(sv_frame_t frame) : + m_haveValue(false), m_haveLevel(false), + m_haveDuration(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_haveDuration(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_haveDuration(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_haveDuration(true), 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_haveDuration(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; + + // We would ideally like Event to be immutable - but we have to + // have these because otherwise we can't put Events in vectors + // etc. Let's call it conceptually immutable + 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_haveValue ? m_value : 0.f; } + + 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_haveDuration; } + sv_frame_t getDuration() const { return m_haveDuration ? m_duration : 0; } + + Event withDuration(sv_frame_t duration) const { + Event p(*this); + p.m_duration = duration; + p.m_haveDuration = true; + if (duration < 0) throw std::logic_error("duration must be >= 0"); + return p; + } + Event withoutDuration() const { + Event p(*this); + p.m_haveDuration = false; + p.m_duration = 0; + return p; + } + + bool hasLabel() const { return m_label != QString(); } + QString getLabel() const { return m_label; } + + Event withLabel(QString label) const { + Event p(*this); + p.m_label = label; + return p; + } + + bool hasUri() const { return m_uri != QString(); } + QString getURI() const { return m_uri; } + + Event withURI(QString uri) const { + Event p(*this); + p.m_uri = uri; + return p; + } + + bool hasLevel() const { return m_haveLevel; } + float getLevel() const { return m_haveLevel ? m_level : 0.f; } + + 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_haveReferenceFrame ? m_referenceFrame : m_frame; + } + + 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_haveDuration != p.m_haveDuration) return false; + if (m_haveDuration && (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; + if (m_uri != p.m_uri) return false; + + return true; + } + + bool operator!=(const Event &p) const { + return !operator==(p); + } + + bool operator<(const Event &p) const { + + if (m_frame != p.m_frame) { + return m_frame < p.m_frame; + } + + // events without a property sort before events with that property + + if (m_haveDuration != p.m_haveDuration) { + return !m_haveDuration; + } + if (m_haveDuration && (m_duration != p.m_duration)) { + return m_duration < p.m_duration; + } + + 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; + } + + if (m_label != p.m_label) { + return m_label < p.m_label; + } + return m_uri < p.m_uri; + } + + struct ExportNameOptions { + + ExportNameOptions() : + valueAtttributeName("value"), + uriAttributeName("uri") { } + + QString valueAtttributeName; + QString uriAttributeName; + }; + + void toXml(QTextStream &stream, + QString indent = "", + QString extraAttributes = "", + ExportNameOptions opts = ExportNameOptions()) const { + + // For I/O purposes these are points, not events + stream << indent << QString("\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) const { + + sv_frame_t duration; + if (m_haveDuration && 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; + } + + QString toDelimitedDataString(QString delimiter, + DataExportOptions opts, + sv_samplerate_t sampleRate) const { + QStringList list; + + list << RealTime::frame2RealTime(m_frame, sampleRate) + .toString().c_str(); + + if (m_haveValue) { + list << QString("%1").arg(m_value); + } + + if (m_haveDuration) { + list << RealTime::frame2RealTime(m_duration, sampleRate) + .toString().c_str(); + } + + if (m_haveLevel) { + if (!(opts & DataExportOmitLevels)) { + list << QString("%1").arg(m_level); + } + } + + if (m_label != "") list << m_label; + if (m_uri != "") list << m_uri; + + return list.join(delimiter); + } + + uint hash(uint seed = 0) const { + uint h = qHash(m_label, seed); + if (m_haveValue) h ^= qHash(m_value); + if (m_haveLevel) h ^= qHash(m_level); + h ^= qHash(m_frame); + if (m_haveDuration) h ^= qHash(m_duration); + if (m_haveReferenceFrame) h ^= qHash(m_referenceFrame); + h ^= qHash(m_uri); + return h; + } + +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_haveDuration : 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; + QString m_uri; +}; + +inline uint qHash(const Event &e, uint seed = 0) { + return e.hash(seed); +} + +typedef std::vector EventVector; + +#endif diff -r ab4fd193262b -r 978c143c767f base/EventSeries.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/base/EventSeries.cpp Fri May 17 10:02:43 2019 +0100 @@ -0,0 +1,582 @@ +/* -*- 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 "EventSeries.h" + +EventSeries +EventSeries::fromEvents(const EventVector &v) +{ + EventSeries s; + for (const auto &e: v) { + s.add(e); + } + return s; +} + +bool +EventSeries::isEmpty() const +{ + return m_events.empty(); +} + +int +EventSeries::count() const +{ + if (m_events.size() > INT_MAX) { + throw std::logic_error("too many events"); + } + return int(m_events.size()); +} + +void +EventSeries::add(const Event &p) +{ + bool isUnique = true; + + auto pitr = lower_bound(m_events.begin(), m_events.end(), p); + if (pitr != m_events.end() && *pitr == p) { + isUnique = false; + } + m_events.insert(pitr, p); + + if (!p.hasDuration() && p.getFrame() > m_finalDurationlessEventFrame) { + m_finalDurationlessEventFrame = p.getFrame(); + } + + if (p.hasDuration() && isUnique) { + + const sv_frame_t frame = p.getFrame(); + const sv_frame_t endFrame = p.getFrame() + p.getDuration(); + + createSeam(frame); + createSeam(endFrame); + + // These calls must both succeed after calling createSeam above + const auto i0 = m_seams.find(frame); + const auto i1 = m_seams.find(endFrame); + + 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.push_back(p); + } + } + +#ifdef DEBUG_EVENT_SERIES + std::cerr << "after add:" << std::endl; + dumpEvents(); + dumpSeams(); +#endif +} + +void +EventSeries::remove(const Event &p) +{ + // If we are removing the last (unique) example of an event, + // then we also need to remove it from the seam map. If this + // is only one of multiple identical events, then we don't. + bool isUnique = true; + + auto pitr = lower_bound(m_events.begin(), m_events.end(), p); + if (pitr == m_events.end() || *pitr != p) { + // we don't know this event + return; + } else { + auto nitr = pitr; + ++nitr; + if (nitr != m_events.end() && *nitr == p) { + isUnique = false; + } + } + + m_events.erase(pitr); + + if (!p.hasDuration() && isUnique && + p.getFrame() == m_finalDurationlessEventFrame) { + m_finalDurationlessEventFrame = 0; + for (auto ritr = m_events.rbegin(); ritr != m_events.rend(); ++ritr) { + if (!ritr->hasDuration()) { + m_finalDurationlessEventFrame = ritr->getFrame(); + break; + } + } + } + + if (p.hasDuration() && isUnique) { + + const sv_frame_t frame = p.getFrame(); + const sv_frame_t endFrame = p.getFrame() + p.getDuration(); + + const auto i0 = m_seams.find(frame); + const 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 + + // Remove any and all instances of p from the seam map; we + // are only supposed to get here if we are removing the + // last instance of p from the series anyway + + 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 + throw std::logic_error("unexpectedly reached end of map"); + } + for (size_t j = 0; j < i->second.size(); ) { + if (i->second[j] == p) { + i->second.erase(i->second.begin() + j); + } else { + ++j; + } + } + } + + // Tidy up by removing any entries that are now identical + // to their predecessors + + std::vector redundant; + + auto pitr = m_seams.end(); + if (i0 != m_seams.begin()) { + pitr = i0; + --pitr; + } + + for (auto i = i0; i != m_seams.end(); ++i) { + if (pitr != m_seams.end() && + seamsEqual(i->second, pitr->second)) { + redundant.push_back(i->first); + } + pitr = i; + if (i == i1) { + break; + } + } + + for (sv_frame_t f: redundant) { + m_seams.erase(f); + } + + // And remove any empty seams from the start of the map + + while (m_seams.begin() != m_seams.end() && + m_seams.begin()->second.empty()) { + m_seams.erase(m_seams.begin()); + } + } + +#ifdef DEBUG_EVENT_SERIES + std::cerr << "after remove:" << std::endl; + dumpEvents(); + dumpSeams(); +#endif +} + +bool +EventSeries::contains(const Event &p) const +{ + return binary_search(m_events.begin(), m_events.end(), p); +} + +void +EventSeries::clear() +{ + m_events.clear(); + m_seams.clear(); + m_finalDurationlessEventFrame = 0; +} + +sv_frame_t +EventSeries::getStartFrame() const +{ + if (m_events.empty()) return 0; + return m_events.begin()->getFrame(); +} + +sv_frame_t +EventSeries::getEndFrame() const +{ + sv_frame_t latest = 0; + + if (m_events.empty()) return latest; + + latest = m_finalDurationlessEventFrame; + + if (m_seams.empty()) return latest; + + sv_frame_t lastSeam = m_seams.rbegin()->first; + if (lastSeam > latest) { + latest = lastSeam; + } + + return latest; +} + +EventVector +EventSeries::getEventsSpanning(sv_frame_t frame, + sv_frame_t duration) const +{ + EventVector span; + + const sv_frame_t start = frame; + const sv_frame_t end = frame + duration; + + // first find any zero-duration events + + auto pitr = lower_bound(m_events.begin(), m_events.end(), + Event(start)); + while (pitr != m_events.end() && pitr->getFrame() < end) { + if (!pitr->hasDuration()) { + span.push_back(*pitr); + } + ++pitr; + } + + // now any non-zero-duration ones from the seam map + + std::set found; + auto sitr = m_seams.lower_bound(start); + if (sitr == m_seams.end() || sitr->first > start) { + if (sitr != m_seams.begin()) { + --sitr; + } + } + while (sitr != m_seams.end() && sitr->first < end) { + for (const auto &p: sitr->second) { + found.insert(p); + } + ++sitr; + } + for (const auto &p: found) { + auto pitr = lower_bound(m_events.begin(), m_events.end(), p); + while (pitr != m_events.end() && *pitr == p) { + span.push_back(p); + ++pitr; + } + } + + return span; +} + +EventVector +EventSeries::getEventsWithin(sv_frame_t frame, + sv_frame_t duration, + int overspill) const +{ + EventVector span; + + const sv_frame_t start = frame; + const sv_frame_t end = frame + duration; + + // because we don't need to "look back" at events that end within + // but started without, we can do this entirely from m_events. + // The core operation is very simple, it's just overspill that + // complicates it. + + Events::const_iterator reference = + lower_bound(m_events.begin(), m_events.end(), Event(start)); + + Events::const_iterator first = reference; + for (int i = 0; i < overspill; ++i) { + if (first == m_events.begin()) break; + --first; + } + for (int i = 0; i < overspill; ++i) { + if (first == reference) break; + span.push_back(*first); + ++first; + } + + Events::const_iterator pitr = reference; + Events::const_iterator last = reference; + + while (pitr != m_events.end() && pitr->getFrame() < end) { + if (!pitr->hasDuration() || + (pitr->getFrame() + pitr->getDuration() <= end)) { + span.push_back(*pitr); + last = pitr; + ++last; + } + ++pitr; + } + + for (int i = 0; i < overspill; ++i) { + if (last == m_events.end()) break; + span.push_back(*last); + ++last; + } + + return span; +} + +EventVector +EventSeries::getEventsStartingWithin(sv_frame_t frame, + sv_frame_t duration) const +{ + EventVector span; + + const sv_frame_t start = frame; + const sv_frame_t end = frame + duration; + + // because we don't need to "look back" at events that started + // earlier than the start of the given range, we can do this + // entirely from m_events + + auto pitr = lower_bound(m_events.begin(), m_events.end(), + Event(start)); + while (pitr != m_events.end() && pitr->getFrame() < end) { + span.push_back(*pitr); + ++pitr; + } + + return span; +} + +EventVector +EventSeries::getEventsCovering(sv_frame_t frame) const +{ + EventVector cover; + + // first find any zero-duration events + + auto pitr = lower_bound(m_events.begin(), m_events.end(), + Event(frame)); + while (pitr != m_events.end() && pitr->getFrame() == frame) { + if (!pitr->hasDuration()) { + cover.push_back(*pitr); + } + ++pitr; + } + + // now any non-zero-duration ones from the seam map + + std::set found; + 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 (const auto &p: sitr->second) { + found.insert(p); + } + ++sitr; + } + for (const auto &p: found) { + auto pitr = lower_bound(m_events.begin(), m_events.end(), p); + while (pitr != m_events.end() && *pitr == p) { + cover.push_back(p); + ++pitr; + } + } + + return cover; +} + +EventVector +EventSeries::getAllEvents() const +{ + return m_events; +} + +bool +EventSeries::getEventPreceding(const Event &e, Event &preceding) const +{ + auto pitr = lower_bound(m_events.begin(), m_events.end(), e); + if (pitr == m_events.end() || *pitr != e) { + return false; + } + if (pitr == m_events.begin()) { + return false; + } + --pitr; + preceding = *pitr; + return true; +} + +bool +EventSeries::getEventFollowing(const Event &e, Event &following) const +{ + auto pitr = lower_bound(m_events.begin(), m_events.end(), e); + if (pitr == m_events.end() || *pitr != e) { + return false; + } + while (*pitr == e) { + ++pitr; + if (pitr == m_events.end()) { + return false; + } + } + following = *pitr; + return true; +} + +bool +EventSeries::getNearestEventMatching(sv_frame_t startSearchAt, + std::function predicate, + Direction direction, + Event &found) const +{ + auto pitr = lower_bound(m_events.begin(), m_events.end(), + Event(startSearchAt)); + + while (true) { + + if (direction == Backward) { + if (pitr == m_events.begin()) { + break; + } else { + --pitr; + } + } else { + if (pitr == m_events.end()) { + break; + } + } + + const Event &e = *pitr; + if (predicate(e)) { + found = e; + return true; + } + + if (direction == Forward) { + ++pitr; + } + } + + return false; +} + +Event +EventSeries::getEventByIndex(int index) const +{ + if (index < 0 || index >= count()) { + throw std::logic_error("index out of range"); + } + return m_events[index]; +} + +int +EventSeries::getIndexForEvent(const Event &e) const +{ + auto pitr = lower_bound(m_events.begin(), m_events.end(), e); + auto d = distance(m_events.begin(), pitr); + if (d < 0 || d > INT_MAX) return 0; + return int(d); +} + +void +EventSeries::toXml(QTextStream &out, + QString indent, + QString extraAttributes) const +{ + toXml(out, indent, extraAttributes, Event::ExportNameOptions()); +} + +void +EventSeries::toXml(QTextStream &out, + QString indent, + QString extraAttributes, + Event::ExportNameOptions options) const +{ + out << indent << QString("\n") + .arg(getExportId()) + .arg(extraAttributes); + + for (const auto &p: m_events) { + p.toXml(out, indent + " ", "", options); + } + + out << indent << "\n"; +} + +QString +EventSeries::toDelimitedDataString(QString delimiter, + DataExportOptions options, + sv_frame_t startFrame, + sv_frame_t duration, + sv_samplerate_t sampleRate, + sv_frame_t resolution, + Event fillEvent) const +{ + QString s; + + const sv_frame_t end = startFrame + duration; + + auto pitr = lower_bound(m_events.begin(), m_events.end(), + Event(startFrame)); + + if (!(options & DataExportFillGaps)) { + + while (pitr != m_events.end() && pitr->getFrame() < end) { + s += pitr->toDelimitedDataString(delimiter, + options, + sampleRate); + s += "\n"; + ++pitr; + } + + } else { + + // find frame time of first point in range (if any) + sv_frame_t first = startFrame; + if (pitr != m_events.end()) { + first = pitr->getFrame(); + } + + // project back to first frame time in range according to + // resolution. e.g. if f0 = 2, first = 9, resolution = 4 then + // we start at 5 (because 1 is too early and we need to arrive + // at 9 to match the first actual point). This method is + // stupid but easy to understand: + sv_frame_t f = first; + while (f >= startFrame + resolution) f -= resolution; + + // now progress, either writing the next point (if within + // distance) or a default fill point + while (f < end) { + if (pitr != m_events.end() && pitr->getFrame() <= f) { + s += pitr->toDelimitedDataString + (delimiter, + options & ~DataExportFillGaps, + sampleRate); + ++pitr; + } else { + s += fillEvent.withFrame(f).toDelimitedDataString + (delimiter, + options & ~DataExportFillGaps, + sampleRate); + } + s += "\n"; + f += resolution; + } + } + + return s; +} + diff -r ab4fd193262b -r 978c143c767f base/EventSeries.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/base/EventSeries.h Fri May 17 10:02:43 2019 +0100 @@ -0,0 +1,354 @@ +/* -*- 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 "XmlExportable.h" + +#include +#include + +//#define DEBUG_EVENT_SERIES 1 + +/** + * Container storing a series of events, with or without durations, + * and supporting the ability to query which events are active at a + * given frame or within a span of frames. + * + * To that end, in addition to the series of events, it stores a + * series of "seams", which are frame positions at which the set of + * simultaneous events changes (i.e. an event of non-zero duration + * starts or ends) associated with a set of the events that are active + * at or from that position. These are updated when an event is added + * or removed. + * + * This class is highly optimised for inserting events in increasing + * order of start frame. Inserting (or deleting) events in the middle + * does work, and should be acceptable in interactive use, but it is + * very slow in bulk. + */ +class EventSeries : public XmlExportable +{ +public: + EventSeries() : m_finalDurationlessEventFrame(0) { } + ~EventSeries() =default; + + EventSeries(const EventSeries &) =default; + EventSeries &operator=(const EventSeries &) =default; + EventSeries &operator=(EventSeries &&) =default; + + bool operator==(const EventSeries &other) const { + return m_events == other.m_events; + } + + static EventSeries fromEvents(const EventVector &ee); + + void clear(); + void add(const Event &e); + void remove(const Event &e); + bool contains(const Event &e) const; + bool isEmpty() const; + int count() const; + + /** + * Return the frame of the first event in the series. If there are + * no events, return 0. + */ + sv_frame_t getStartFrame() const; + + /** + * Return the frame plus duration of the event in the series that + * ends last. If there are no events, return 0. + */ + sv_frame_t getEndFrame() const; + + /** + * Retrieve all events any part of which falls within the range in + * frames defined by the given frame f and duration d. + * + * - An event without duration is spanned by the range if its own + * frame is greater than or equal to f and less than f + d. + * + * - An event with duration is spanned by the range if its start + * frame is less than f + d and its start frame plus its duration + * is greater than f. + * + * Note: Passing a duration of zero is seldom useful here; you + * probably want getEventsCovering instead. getEventsSpanning(f, + * 0) is not equivalent to getEventsCovering(f). The latter + * includes durationless events at f and events starting at f, + * both of which are excluded from the former. + */ + EventVector getEventsSpanning(sv_frame_t frame, + sv_frame_t duration) const; + + /** + * Retrieve all events that cover the given frame. An event without + * duration covers a frame if its own frame is equal to it. An event + * with duration covers a frame if its start frame is less than or + * equal to it and its end frame (start + duration) is greater + * than it. + */ + EventVector getEventsCovering(sv_frame_t frame) const; + + /** + * Retrieve all events falling wholly within the range in frames + * defined by the given frame f and duration d. + * + * - An event without duration is within the range if its own + * frame is greater than or equal to f and less than f + d. + * + * - An event with duration is within the range if its start frame + * is greater than or equal to f and its start frame plus its + * duration is less than or equal to f + d. + * + * If overspill is greater than zero, also include that number of + * additional events (where they exist) both before and after the + * edges of the range. + */ + EventVector getEventsWithin(sv_frame_t frame, + sv_frame_t duration, + int overspill = 0) const; + + /** + * Retrieve all events starting within the range in frames defined + * by the given frame f and duration d. An event (regardless of + * whether it has duration or not) starts within the range if its + * start frame is greater than or equal to f and less than f + d. + */ + EventVector getEventsStartingWithin(sv_frame_t frame, + sv_frame_t duration) const; + + /** + * Retrieve all events starting at exactly the given frame. + */ + EventVector getEventsStartingAt(sv_frame_t frame) const { + return getEventsStartingWithin(frame, 1); + } + + /** + * Retrieve all events, in their natural order. + */ + EventVector getAllEvents() const; + + /** + * If e is in the series and is not the first event in it, set + * preceding to the event immediate preceding it according to the + * standard event ordering and return true. Otherwise leave + * preceding unchanged and return false. + * + * If there are multiple events identical to e in the series, + * assume that the event passed in is the first one (i.e. never + * set preceding equal to e). + * + * It is acceptable for preceding to alias e when this is called. + */ + bool getEventPreceding(const Event &e, Event &preceding) const; + + /** + * If e is in the series and is not the final event in it, set + * following to the event immediate following it according to the + * standard event ordering and return true. Otherwise leave + * following unchanged and return false. + * + * If there are multiple events identical to e in the series, + * assume that the event passed in is the last one (i.e. never set + * following equal to e). + * + * It is acceptable for following to alias e when this is called. + */ + bool getEventFollowing(const Event &e, Event &following) const; + + enum Direction { + Forward, + Backward + }; + + /** + * Return the first event for which the given predicate returns + * true, searching events with start frames increasingly far from + * the given frame in the given direction. If the direction is + * Forward then the search includes events starting at the given + * frame, otherwise it does not. + */ + bool getNearestEventMatching(sv_frame_t startSearchAt, + std::function predicate, + Direction direction, + Event &found) const; + + /** + * Return the event at the given numerical index in the series, + * where 0 = the first event and count()-1 = the last. + */ + Event getEventByIndex(int index) const; + + /** + * Return the index of the first event in the series that does not + * compare inferior to the given event. If there is no such event, + * return count(). + */ + int getIndexForEvent(const Event &e) const; + + /** + * Emit to XML as a dataset element. + */ + void toXml(QTextStream &out, + QString indent, + QString extraAttributes) const override; + + /** + * Emit to XML as a dataset element. + */ + void toXml(QTextStream &out, + QString indent, + QString extraAttributes, + Event::ExportNameOptions) const; + + /** + * Emit events starting within the given range to a delimited + * (e.g. comma-separated) data format. + */ + QString toDelimitedDataString(QString delimiter, + DataExportOptions options, + sv_frame_t startFrame, + sv_frame_t duration, + sv_samplerate_t sampleRate, + sv_frame_t resolution, + Event fillEvent) const; + +private: + /** + * This vector contains all events in the series, in the normal + * sort order. For backward compatibility we must support series + * containing multiple instances of identical events, so + * consecutive events in this vector will not always be distinct. + * The vector is used in preference to a multiset or map in order to allow indexing by "row number" as well as by + * properties such as frame. + * + * Because events are immutable, we do not have to worry about the + * order changing once an event is inserted - we only add or + * delete them. + */ + typedef std::vector Events; + Events m_events; + + /** + * The FrameEventMap maps from frame number to a set of events. In + * the seam map this is used to represent the events that are + * active at that frame, either because they begin at that frame + * or because they are continuing from an earlier frame. There is + * an entry here for each frame at which an event starts or ends, + * with the event appearing in all entries from its start time + * onward and disappearing again at its end frame. + * + * Only events with duration appear in this map; point events + * appear only in m_events. Note that unlike m_events, we only + * store one instance of each event here, even if we hold many - + * we refer back to m_events when we need to know how many + * identical copies of a given event we have. + */ + typedef std::map> FrameEventMap; + FrameEventMap m_seams; + + /** + * The frame of the last durationless event we have in the series. + * This is to support a fast-ish getEndFrame(): we can easily keep + * this up-to-date when events are added or removed, and we can + * easily find the end frame of the last with-duration event from + * the seam map, but it's not so easy to continuously update an + * overall end frame or to find the last frame of all events + * without this. + */ + sv_frame_t m_finalDurationlessEventFrame; + + /** 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] = {}; + } + } + + bool seamsEqual(const std::vector &s1, + const std::vector &s2) const { + + if (s1.size() != s2.size()) { + return false; + } + + // precondition: no event appears more than once in s1 or more + // than once in s2 + +#ifdef DEBUG_EVENT_SERIES + for (int i = 0; in_range_for(s1, i); ++i) { + for (int j = i + 1; in_range_for(s1, j); ++j) { + if (s1[i] == s1[j] || s2[i] == s2[j]) { + throw std::runtime_error + ("debug error: duplicate event in s1 or s2"); + } + } + } +#endif + + std::set ee; + for (const auto &e: s1) { + ee.insert(e); + } + for (const auto &e: s2) { + if (ee.find(e) == ee.end()) { + return false; + } + } + return true; + } + +#ifdef DEBUG_EVENT_SERIES + void dumpEvents() const { + std::cerr << "EVENTS (" << m_events.size() << ") [" << std::endl; + for (const auto &i: m_events) { + std::cerr << " " << i.toXmlString(); + } + std::cerr << "]" << std::endl; + } + + void dumpSeams() const { + std::cerr << "SEAMS (" << m_seams.size() << ") [" << 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 diff -r ab4fd193262b -r 978c143c767f base/Extents.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/base/Extents.h Fri May 17 10:02:43 2019 +0100 @@ -0,0 +1,92 @@ +/* -*- 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_EXTENTS_H +#define SV_EXTENTS_H + +#include + +/** + * Maintain a min and max value, and update them when supplied a new + * data point. + */ +template +class Extents +{ +public: + Extents() : m_min(T()), m_max(T()) { } + Extents(T min, T max) : m_min(min), m_max(max) { } + + bool operator==(const Extents &r) { + return r.m_min == m_min && r.m_max == m_max; + } + bool operator!=(const Extents &r) { + return !(*this == r); + } + + bool isSet() const { + return (m_min != T() || m_max != T()); + } + void set(T min, T max) { + m_min = min; + m_max = max; + if (m_max < m_min) m_max = m_min; + } + void reset() { + m_min = T(); + m_max = T(); + } + + bool sample(T f) { + bool changed = false; + if (isSet()) { + if (f < m_min) { m_min = f; changed = true; } + if (f > m_max) { m_max = f; changed = true; } + } else { + m_max = m_min = f; + changed = true; + } + return changed; + } + bool sample(const std::vector &ff) { + bool changed = false; + for (auto f: ff) { + if (sample(f)) { + changed = true; + } + } + return changed; + } + bool sample(const Extents &r) { + bool changed = false; + if (isSet()) { + if (r.m_min < m_min) { m_min = r.m_min; changed = true; } + if (r.m_max > m_max) { m_max = r.m_max; changed = true; } + } else { + m_min = r.m_min; + m_max = r.m_max; + changed = true; + } + return changed; + } + + T getMin() const { return m_min; } + T getMax() const { return m_max; } + +private: + T m_min; + T m_max; +}; + +#endif diff -r ab4fd193262b -r 978c143c767f base/HelperExecPath.cpp --- a/base/HelperExecPath.cpp Thu May 16 12:54:58 2019 +0100 +++ b/base/HelperExecPath.cpp Fri May 17 10:02:43 2019 +0100 @@ -58,9 +58,17 @@ QStringList HelperExecPath::getHelperDirPaths() { + // Helpers are expected to exist either in the same directory as + // this executable was found, or in either a subdirectory called + // helpers, or on the Mac only, a sibling called Resources. + QStringList dirs; QString myDir = QCoreApplication::applicationDirPath(); +#ifdef Q_OS_MAC + dirs.push_back(myDir + "/../Resources"); +#else dirs.push_back(myDir + "/helpers"); +#endif dirs.push_back(myDir); return dirs; } @@ -76,9 +84,6 @@ QList HelperExecPath::search(QString basename, QStringList &candidates) { - // Helpers are expected to exist either in the same directory as - // this executable was found, or in a subdirectory called helpers. - QString extension = ""; #ifdef _WIN32 extension = ".exe"; diff -r ab4fd193262b -r 978c143c767f base/MagnitudeRange.h --- a/base/MagnitudeRange.h Thu May 16 12:54:58 2019 +0100 +++ b/base/MagnitudeRange.h Fri May 17 10:02:43 2019 +0100 @@ -16,68 +16,8 @@ #ifndef MAGNITUDE_RANGE_H #define MAGNITUDE_RANGE_H -#include +#include "Extents.h" -/** - * Maintain a min and max value, and update them when supplied a new - * data point. - */ -class MagnitudeRange -{ -public: - MagnitudeRange() : m_min(0), m_max(0) { } - MagnitudeRange(float min, float max) : m_min(min), m_max(max) { } - - bool operator==(const MagnitudeRange &r) { - return r.m_min == m_min && r.m_max == m_max; - } - bool operator!=(const MagnitudeRange &r) { - return !(*this == r); - } - - bool isSet() const { return (m_min != 0.f || m_max != 0.f); } - void set(float min, float max) { - m_min = min; - m_max = max; - if (m_max < m_min) m_max = m_min; - } - bool sample(float f) { - bool changed = false; - if (isSet()) { - if (f < m_min) { m_min = f; changed = true; } - if (f > m_max) { m_max = f; changed = true; } - } else { - m_max = m_min = f; - changed = true; - } - return changed; - } - bool sample(const std::vector &ff) { - bool changed = false; - for (auto f: ff) { - if (sample(f)) { - changed = true; - } - } - return changed; - } - bool sample(const MagnitudeRange &r) { - bool changed = false; - if (isSet()) { - if (r.m_min < m_min) { m_min = r.m_min; changed = true; } - if (r.m_max > m_max) { m_max = r.m_max; changed = true; } - } else { - m_min = r.m_min; - m_max = r.m_max; - changed = true; - } - return changed; - } - float getMin() const { return m_min; } - float getMax() const { return m_max; } -private: - float m_min; - float m_max; -}; +typedef Extents MagnitudeRange; #endif diff -r ab4fd193262b -r 978c143c767f base/NoteData.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/base/NoteData.h Fri May 17 10:02:43 2019 +0100 @@ -0,0 +1,50 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + + This 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 + +#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 NoteList; + +#endif diff -r ab4fd193262b -r 978c143c767f base/NoteExportable.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/base/NoteExportable.h Fri May 17 10:02:43 2019 +0100 @@ -0,0 +1,42 @@ +/* -*- 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_EXPORTABLE_H +#define SV_NOTE_EXPORTABLE_H + +#include "NoteData.h" + +class NoteExportable +{ +public: + /** + * Get all notes in the exportable object. + */ + virtual NoteList getNotes() const = 0; + + /** + * Get notes that are active at the given frame, i.e. that start + * before or at this frame and have not ended by it. + */ + virtual NoteList getNotesActiveAt(sv_frame_t frame) const = 0; + + /** + * Get notes that start within the range in frames defined by the + * given start frame and duration. + */ + virtual NoteList getNotesStartingWithin(sv_frame_t startFrame, + sv_frame_t duration) const = 0; +}; + +#endif diff -r ab4fd193262b -r 978c143c767f base/RecentFiles.cpp --- a/base/RecentFiles.cpp Thu May 16 12:54:58 2019 +0100 +++ b/base/RecentFiles.cpp Fri May 17 10:02:43 2019 +0100 @@ -20,6 +20,7 @@ #include #include #include +#include RecentFiles::RecentFiles(QString settingsGroup, int maxCount) : m_settingsGroup(settingsGroup), @@ -36,16 +37,26 @@ void RecentFiles::read() { - m_names.clear(); + // Private method - called only from constructor - no mutex lock required + + m_entries.clear(); QSettings settings; settings.beginGroup(m_settingsGroup); for (int i = 0; i < 100; ++i) { - QString key = QString("recent-%1").arg(i); - QString name = settings.value(key, "").toString(); - if (name == "") break; - if (i < m_maxCount) m_names.push_back(name); - else settings.setValue(key, ""); + + QString idKey = QString("recent-%1").arg(i); + QString identifier = settings.value(idKey, "").toString(); + if (identifier == "") break; + + QString labelKey = QString("recent-%1-label").arg(i); + QString label = settings.value(labelKey, "").toString(); + + if (i < m_maxCount) m_entries.push_back({ identifier, label }); + else { + settings.setValue(idKey, ""); + settings.setValue(labelKey, ""); + } } settings.endGroup(); @@ -54,14 +65,22 @@ void RecentFiles::write() { + // Private method - must be serialised at call site + QSettings settings; settings.beginGroup(m_settingsGroup); for (int i = 0; i < m_maxCount; ++i) { - QString key = QString("recent-%1").arg(i); - QString name = ""; - if (i < (int)m_names.size()) name = m_names[i]; - settings.setValue(key, name); + QString idKey = QString("recent-%1").arg(i); + QString labelKey = QString("recent-%1-label").arg(i); + QString identifier; + QString label; + if (in_range_for(m_entries, i)) { + identifier = m_entries[i].first; + label = m_entries[i].second; + } + settings.setValue(idKey, identifier); + settings.setValue(labelKey, label); } settings.endGroup(); @@ -70,67 +89,92 @@ void RecentFiles::truncateAndWrite() { - while (int(m_names.size()) > m_maxCount) { - m_names.pop_back(); + // Private method - must be serialised at call site + + while (int(m_entries.size()) > m_maxCount) { + m_entries.pop_back(); } write(); } std::vector -RecentFiles::getRecent() const +RecentFiles::getRecentIdentifiers() const { - std::vector names; + QMutexLocker locker(&m_mutex); + + std::vector identifiers; for (int i = 0; i < m_maxCount; ++i) { - if (i < (int)m_names.size()) { - names.push_back(m_names[i]); + if (i < (int)m_entries.size()) { + identifiers.push_back(m_entries[i].first); } } - return names; + + return identifiers; +} + +std::vector> +RecentFiles::getRecentEntries() const +{ + QMutexLocker locker(&m_mutex); + + std::vector> entries; + for (int i = 0; i < m_maxCount; ++i) { + if (i < (int)m_entries.size()) { + entries.push_back(m_entries[i]); + } + } + + return entries; } void -RecentFiles::add(QString name) +RecentFiles::add(QString identifier, QString label) { - bool have = false; - for (int i = 0; i < int(m_names.size()); ++i) { - if (m_names[i] == name) { - have = true; - break; + { + QMutexLocker locker(&m_mutex); + + bool have = false; + for (int i = 0; i < int(m_entries.size()); ++i) { + if (m_entries[i].first == identifier) { + have = true; + break; + } } + + if (!have) { + m_entries.push_front({ identifier, label }); + } else { + std::deque> newEntries; + newEntries.push_back({ identifier, label }); + for (int i = 0; in_range_for(m_entries, i); ++i) { + if (m_entries[i].first == identifier) continue; + newEntries.push_back(m_entries[i]); + } + m_entries = newEntries; + } + + truncateAndWrite(); } - if (!have) { - m_names.push_front(name); - } else { - std::deque newnames; - newnames.push_back(name); - for (int i = 0; i < int(m_names.size()); ++i) { - if (m_names[i] == name) continue; - newnames.push_back(m_names[i]); - } - m_names = newnames; - } - - truncateAndWrite(); emit recentChanged(); } void -RecentFiles::addFile(QString name) +RecentFiles::addFile(QString filepath, QString label) { static QRegExp schemeRE("^[a-zA-Z]{2,5}://"); static QRegExp tempRE("[\\/][Tt]e?mp[\\/]"); - if (schemeRE.indexIn(name) == 0) { - add(name); + if (schemeRE.indexIn(filepath) == 0) { + add(filepath, label); } else { - QString absPath = QFileInfo(name).absoluteFilePath(); + QString absPath = QFileInfo(filepath).absoluteFilePath(); if (tempRE.indexIn(absPath) != -1) { Preferences *prefs = Preferences::getInstance(); if (prefs && !prefs->getOmitTempsFromRecentFiles()) { - add(absPath); + add(absPath, label); } } else { - add(absPath); + add(absPath, label); } } } diff -r ab4fd193262b -r 978c143c767f base/RecentFiles.h --- a/base/RecentFiles.h Thu May 16 12:54:58 2019 +0100 +++ b/base/RecentFiles.h Fri May 17 10:02:43 2019 +0100 @@ -18,15 +18,21 @@ #include #include +#include #include #include /** - * RecentFiles manages a list of the names of recently-used objects, - * saving and restoring that list via QSettings. The names do not - * actually have to refer to files. + * RecentFiles manages a list of recently-used identifier strings, + * saving and restoring that list via QSettings. The identifiers do + * not actually have to refer to files. + * + * Each entry must have a non-empty identifier, which is typically a + * filename, path, URI, or internal id, and may optionally also have a + * label, which is typically a user-visible convenience. + * + * RecentFiles is thread-safe - all access is serialised. */ - class RecentFiles : public QObject { Q_OBJECT @@ -35,42 +41,81 @@ /** * Construct a RecentFiles object that saves and restores in the * given QSettings group and truncates when the given count of - * strings is reached. + * identifiers is reached. */ - RecentFiles(QString settingsGroup = "RecentFiles", int maxCount = 10); + RecentFiles(QString settingsGroup = "RecentFiles", + int maxCount = 10); virtual ~RecentFiles(); - QString getSettingsGroup() const { return m_settingsGroup; } - - int getMaxCount() const { return m_maxCount; } - - std::vector getRecent() const; + /** + * Return the settingsGroup as passed to the constructor. + */ + QString getSettingsGroup() const { + return m_settingsGroup; + } /** - * Add a name that should be treated as a literal string. + * Return the maxCount as passed to the constructor. */ - void add(QString name); + int getMaxCount() const { + return m_maxCount; + } + + /** + * Return the list of recent identifiers, without labels. + */ + std::vector getRecentIdentifiers() const; + + /** + * Return the list of recent identifiers, without labels. This is + * an alias for getRecentIdentifiers included for backward + * compatibility. + */ + std::vector getRecent() const { + return getRecentIdentifiers(); + } + + /** + * Return the list of recent identifiers, with labels. Each + * returned entry is a pair of identifier and label in that order. + */ + std::vector> getRecentEntries() const; /** - * Add a name that is known to be either a file path or a URL. If - * it looks like a URL, add it literally; otherwise treat it as a - * file path and canonicalise it appropriately. Also takes into - * account the user preference for whether to include temporary - * files in the recent files menu: the file will not be added if - * the preference is set and the file appears to be a temporary - * one. + * Add a literal identifier, optionally with a label. + * + * If the identifier already exists in the recent entries list, it + * is moved to the front of the list and its label is replaced + * with the given one. */ - void addFile(QString name); + void add(QString identifier, QString label = ""); + + /** + * Add a name that is known to be either a file path or a URL, + * optionally with a label. If it looks like a URL, add it + * literally; otherwise treat it as a file path and canonicalise + * it appropriately. Also take into account the user preference + * for whether to include temporary files in the recent files + * menu: the file will not be added if the preference is set and + * the file appears to be a temporary one. + * + * If the identifier derived from the file path already exists in + * the recent entries list, it is moved to the front of the list + * and its label is replaced with the given one. + */ + void addFile(QString filepath, QString label = ""); signals: void recentChanged(); -protected: - QString m_settingsGroup; - int m_maxCount; +private: + mutable QMutex m_mutex; - std::deque m_names; + const QString m_settingsGroup; + const int m_maxCount; + + std::deque> m_entries; // identifier, label void read(); void write(); diff -r ab4fd193262b -r 978c143c767f base/Selection.h --- a/base/Selection.h Thu May 16 12:54:58 2019 +0100 +++ b/base/Selection.h Fri May 17 10:02:43 2019 +0100 @@ -49,6 +49,7 @@ bool isEmpty() const; sv_frame_t getStartFrame() const; sv_frame_t getEndFrame() const; + sv_frame_t getDuration() const { return getEndFrame() - getStartFrame(); } bool contains(sv_frame_t frame) const; bool operator<(const Selection &) const; diff -r ab4fd193262b -r 978c143c767f base/XmlExportable.cpp --- a/base/XmlExportable.cpp Thu May 16 12:54:58 2019 +0100 +++ b/base/XmlExportable.cpp Fri May 17 10:02:43 2019 +0100 @@ -68,19 +68,18 @@ } int -XmlExportable::getObjectExportId(const void * object) +XmlExportable::getExportId() const { - static QMutex mutex; - QMutexLocker locker(&mutex); - - static std::map idMap; - static int maxId = 0; - - if (idMap.find(object) == idMap.end()) { - idMap[object] = maxId++; + if (m_exportId == -1) { + static QMutex mutex; + static int nextId = 0; + QMutexLocker locker(&mutex); + if (m_exportId == -1) { + m_exportId = nextId; + ++nextId; + } } - - return idMap[object]; + return m_exportId; } diff -r ab4fd193262b -r 978c143c767f base/XmlExportable.h --- a/base/XmlExportable.h Thu May 16 12:54:58 2019 +0100 +++ b/base/XmlExportable.h Fri May 17 10:02:43 2019 +0100 @@ -25,9 +25,17 @@ class XmlExportable { public: + XmlExportable() : m_exportId(-1) { } virtual ~XmlExportable() { } /** + * Return the numerical export identifier for this object. It's + * allocated the first time this is called, so objects on which + * this is never called do not get allocated one. + */ + int getExportId() const; + + /** * Stream this exportable object out to XML on a text stream. */ virtual void toXml(QTextStream &stream, @@ -46,7 +54,8 @@ static QString encodeColour(int r, int g, int b); - static int getObjectExportId(const void *); // thread-safe +private: + mutable int m_exportId; }; #endif diff -r ab4fd193262b -r 978c143c767f base/test/StressEventSeries.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/base/test/StressEventSeries.h Fri May 17 10:02:43 2019 +0100 @@ -0,0 +1,84 @@ +/* -*- 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 STRESS_EVENT_SERIES_H +#define STRESS_EVENT_SERIES_H + +#include "../EventSeries.h" + +#include +#include + +#include + +using namespace std; + +class StressEventSeries : public QObject +{ + Q_OBJECT + +private: + void report(int n, QString sort, clock_t start, clock_t end) { + QString message = QString("Time for %1 %2 events = ").arg(n).arg(sort); + cerr << " " << message; + for (int i = 0; i < 34 - message.size(); ++i) cerr << " "; + cerr << double(end - start) * 1000.0 / double(CLOCKS_PER_SEC) + << "ms" << std::endl; + } + + void short_n(int n) { + clock_t start = clock(); + std::set ee; + EventSeries s; + for (int i = 0; i < n; ++i) { + float value = float(rand()) / float(RAND_MAX); + Event e(rand(), value, 1000, QString("event %1").arg(i)); + ee.insert(e); + } + for (const Event &e: ee) { + s.add(e); + } + QCOMPARE(s.count(), n); + clock_t end = clock(); + report(n, "short", start, end); + } + + void longish_n(int n) { + clock_t start = clock(); + std::set ee; + EventSeries s; + for (int i = 0; i < n; ++i) { + float value = float(rand()) / float(RAND_MAX); + Event e(rand(), value, rand() / 1000, QString("event %1").arg(i)); + ee.insert(e); + } + for (const Event &e: ee) { + s.add(e); + } + QCOMPARE(s.count(), n); + clock_t end = clock(); + report(n, "longish", start, end); + } + +private slots: + void short_3() { short_n(1000); } + void short_4() { short_n(10000); } + void short_5() { short_n(100000); } + void short_6() { short_n(1000000); } + void longish_3() { longish_n(1000); } + void longish_4() { longish_n(10000); } + void longish_5() { longish_n(100000); } +}; + +#endif diff -r ab4fd193262b -r 978c143c767f base/test/TestEventSeries.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/base/test/TestEventSeries.h Fri May 17 10:02:43 2019 +0100 @@ -0,0 +1,747 @@ +/* -*- 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 +#include + +#include + +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.getEventsCovering(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.count(), 0); + QCOMPARE(s.contains(p), false); + } + + void duplicateEvents() { + + EventSeries s; + Event p(10, QString()); + s.add(p); + s.add(p); + QCOMPARE(s.isEmpty(), false); + QCOMPARE(s.count(), 2); + QCOMPARE(s.contains(p), true); + + s.remove(p); + QCOMPARE(s.isEmpty(), false); + QCOMPARE(s.count(), 1); + QCOMPARE(s.contains(p), true); + + s.remove(p); + QCOMPARE(s.isEmpty(), true); + QCOMPARE(s.count(), 0); + QCOMPARE(s.contains(p), false); + } + + void singleEventCover() { + + EventSeries s; + Event p(10, QString()); + s.add(p); + EventVector cover; + cover.push_back(p); + QCOMPARE(s.getEventsCovering(10), cover); + QCOMPARE(s.getEventsCovering(11), EventVector()); + QCOMPARE(s.getEventsCovering(9), EventVector()); + } + + void singleEventSpan() { + + EventSeries s; + Event p(10, QString()); + s.add(p); + EventVector span; + span.push_back(p); + QCOMPARE(s.getEventsSpanning(10, 2), span); + QCOMPARE(s.getEventsSpanning(9, 2), span); + QCOMPARE(s.getEventsSpanning(8, 2), EventVector()); + QCOMPARE(s.getEventsSpanning(7, 2), EventVector()); + QCOMPARE(s.getEventsSpanning(11, 2), EventVector()); + } + + void identicalEventsCover() { + + EventSeries s; + Event p(10, QString()); + s.add(p); + s.add(p); + + EventVector cover; + cover.push_back(p); + cover.push_back(p); + QCOMPARE(s.getEventsCovering(10), cover); + QCOMPARE(s.getEventsCovering(11), EventVector()); + QCOMPARE(s.getEventsCovering(9), EventVector()); + + s.remove(p); + cover.clear(); + cover.push_back(p); + QCOMPARE(s.getEventsCovering(10), cover); + QCOMPARE(s.getEventsCovering(11), EventVector()); + QCOMPARE(s.getEventsCovering(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, 2), span); + QCOMPARE(s.getEventsSpanning(9, 2), span); + QCOMPARE(s.getEventsSpanning(8, 2), EventVector()); + QCOMPARE(s.getEventsSpanning(11, 2), EventVector()); + } + + void similarEventsCover() { + + EventSeries s; + Event a(10, QString("a")); + Event b(10, QString("b")); + s.add(a); + s.add(b); + EventVector cover; + cover.push_back(a); + cover.push_back(b); + QCOMPARE(s.getEventsCovering(10), cover); + QCOMPARE(s.getEventsCovering(11), EventVector()); + QCOMPARE(s.getEventsCovering(9), EventVector()); + } + + void similarEventsSpan() { + + EventSeries s; + Event a(10, QString("a")); + Event b(10, QString("b")); + s.add(a); + s.add(b); + EventVector span; + span.push_back(a); + span.push_back(b); + QCOMPARE(s.getEventsSpanning(10, 2), span); + QCOMPARE(s.getEventsSpanning(9, 2), span); + QCOMPARE(s.getEventsSpanning(11, 2), EventVector()); + QCOMPARE(s.getEventsSpanning(8, 2), EventVector()); + } + + void singleEventWithDurationCover() { + + EventSeries s; + Event p(10, 1.0, 20, QString()); + s.add(p); + EventVector cover; + cover.push_back(p); + QCOMPARE(s.getEventsCovering(10), cover); + QCOMPARE(s.getEventsCovering(11), cover); + QCOMPARE(s.getEventsCovering(29), cover); + QCOMPARE(s.getEventsCovering(30), EventVector()); + QCOMPARE(s.getEventsCovering(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(9, 2), span); + QCOMPARE(s.getEventsSpanning(8, 2), EventVector()); + QCOMPARE(s.getEventsSpanning(19, 4), span); + QCOMPARE(s.getEventsSpanning(29, 2), span); + QCOMPARE(s.getEventsSpanning(30, 2), EventVector()); + } + + void identicalEventsWithDurationCover() { + + EventSeries s; + Event p(10, 1.0, 20, QString()); + s.add(p); + s.add(p); + EventVector cover; + cover.push_back(p); + cover.push_back(p); + QCOMPARE(s.getEventsCovering(10), cover); + QCOMPARE(s.getEventsCovering(11), cover); + QCOMPARE(s.getEventsCovering(29), cover); + QCOMPARE(s.getEventsCovering(30), EventVector()); + QCOMPARE(s.getEventsCovering(9), EventVector()); + + s.remove(p); + cover.clear(); + cover.push_back(p); + QCOMPARE(s.getEventsCovering(10), cover); + QCOMPARE(s.getEventsCovering(11), cover); + QCOMPARE(s.getEventsCovering(29), cover); + QCOMPARE(s.getEventsCovering(30), EventVector()); + QCOMPARE(s.getEventsCovering(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(9, 2), span); + QCOMPARE(s.getEventsSpanning(10, 2), span); + QCOMPARE(s.getEventsSpanning(11, 2), span); + QCOMPARE(s.getEventsSpanning(29, 2), span); + QCOMPARE(s.getEventsSpanning(30, 2), EventVector()); + QCOMPARE(s.getEventsSpanning(8, 2), EventVector()); + } + + void multipleEventsCover() { + + 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 cover; + cover.push_back(a); + QCOMPARE(s.getEventsCovering(10), cover); + cover.clear(); + cover.push_back(c); + QCOMPARE(s.getEventsCovering(40), cover); + QCOMPARE(s.getEventsCovering(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); + EventVector span; + span.push_back(a); + span.push_back(b); + QCOMPARE(s.getEventsSpanning(10, 2), span); + span.clear(); + span.push_back(c); + QCOMPARE(s.getEventsSpanning(39, 3), span); + QCOMPARE(s.getEventsSpanning(9, 1), EventVector()); + QCOMPARE(s.getEventsSpanning(10, 0), EventVector()); + } + + void multipleEventsEndFrame() { + + 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.add(b); + QCOMPARE(s.getEndFrame(), sv_frame_t(40)); + s.remove(c); + QCOMPARE(s.getEndFrame(), sv_frame_t(11)); + s.remove(b); + QCOMPARE(s.getEndFrame(), sv_frame_t(11)); + s.remove(a); + QCOMPARE(s.getEndFrame(), sv_frame_t(11)); + s.remove(b); + QCOMPARE(s.getEndFrame(), sv_frame_t(0)); + } + + void disjointEventsWithDurationCover() { + + 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.getEventsCovering(0), EventVector()); + QCOMPARE(s.getEventsCovering(10), EventVector({ a })); + QCOMPARE(s.getEventsCovering(15), EventVector({ a })); + QCOMPARE(s.getEventsCovering(30), EventVector()); + QCOMPARE(s.getEventsCovering(99), EventVector()); + QCOMPARE(s.getEventsCovering(100), EventVector({ b })); + QCOMPARE(s.getEventsCovering(120), EventVector({ b })); + QCOMPARE(s.getEventsCovering(130), 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, 10), EventVector()); + QCOMPARE(s.getEventsSpanning(10, 10), EventVector({ a })); + QCOMPARE(s.getEventsSpanning(15, 85), EventVector({ a })); + QCOMPARE(s.getEventsSpanning(30, 5), EventVector()); + QCOMPARE(s.getEventsSpanning(99, 1), EventVector()); + QCOMPARE(s.getEventsSpanning(100, 1), EventVector({ b })); + QCOMPARE(s.getEventsSpanning(120, 20), EventVector({ b })); + QCOMPARE(s.getEventsSpanning(130, 109), EventVector()); + } + + void overlappingEventsWithAndWithoutDurationCover() { + + EventSeries s; + Event p(20, QString("p")); + Event a(10, 1.0f, 20, QString("a")); + s.add(p); + s.add(a); + EventVector cover; + cover.push_back(a); + QCOMPARE(s.getEventsCovering(15), cover); + QCOMPARE(s.getEventsCovering(25), cover); + cover.clear(); + cover.push_back(p); + cover.push_back(a); + QCOMPARE(s.getEventsCovering(20), cover); + } + + void overlappingEventsWithAndWithoutDurationSpan() { + + EventSeries s; + Event p(20, QString("p")); + Event a(10, 1.0f, 20, QString("a")); + s.add(p); + s.add(a); + EventVector span; + span.push_back(a); + QCOMPARE(s.getEventsSpanning(5, 10), span); + QCOMPARE(s.getEventsSpanning(25, 5), span); + span.clear(); + span.push_back(p); + span.push_back(a); + QCOMPARE(s.getEventsSpanning(20, 1), span); + } + + void overlappingEventsWithDurationCover() { + + EventSeries s; + Event a(20, 1.0f, 10, QString("a")); + Event b(10, 1.0f, 20, QString("b")); + Event c(10, 1.0f, 40, QString("c")); + s.add(a); + s.add(b); + s.add(c); + QCOMPARE(s.getEventsCovering(10), EventVector({ b, c })); + QCOMPARE(s.getEventsCovering(20), EventVector({ b, c, a })); + QCOMPARE(s.getEventsCovering(25), EventVector({ b, c, a })); + QCOMPARE(s.getEventsCovering(30), EventVector({ c })); + QCOMPARE(s.getEventsCovering(40), EventVector({ c })); + QCOMPARE(s.getEventsCovering(50), EventVector()); + } + + void overlappingEventsWithDurationSpan() { + + EventSeries s; + Event a(20, 1.0f, 10, QString("a")); + Event b(10, 1.0f, 20, QString("b")); + Event c(10, 1.0f, 40, QString("c")); + s.add(a); + s.add(b); + s.add(c); + QCOMPARE(s.getEventsSpanning(10, 5), EventVector({ b, c })); + QCOMPARE(s.getEventsSpanning(20, 15), EventVector({ b, c, a })); + QCOMPARE(s.getEventsSpanning(0, 100), EventVector({ b, c, a })); + QCOMPARE(s.getEventsSpanning(25, 4), EventVector({ b, c, a })); + QCOMPARE(s.getEventsSpanning(30, 4), EventVector({ c })); + QCOMPARE(s.getEventsSpanning(40, 15), EventVector({ c })); + QCOMPARE(s.getEventsSpanning(50, 10), EventVector()); + } + + void eventPatternCover() { + + EventSeries s; + Event a(0, 1.0f, 18, QString("a")); + Event b(3, 2.0f, 6, QString("b")); + Event c(5, 3.0f, 2, QString("c")); + Event cc(5, 3.1f, 2, QString("cc")); + Event d(6, 4.0f, 10, QString("d")); + Event dd(6, 4.5f, 10, QString("dd")); + Event e(14, 5.0f, 3, QString("e")); + s.add(b); + s.add(c); + s.add(d); + s.add(a); + s.add(cc); + s.add(dd); + s.add(e); + QCOMPARE(s.getEventsCovering(8), EventVector({ a, b, d, dd })); + } + + void eventPatternSpan() { + + EventSeries s; + Event a(0, 1.0f, 18, QString("a")); + Event b(3, 2.0f, 6, QString("b")); + Event c(5, 3.0f, 2, QString("c")); + Event cc(5, 3.1f, 2, QString("cc")); + Event d(6, 4.0f, 10, QString("d")); + Event dd(6, 4.5f, 10, QString("dd")); + Event e(14, 5.0f, 3, QString("e")); + s.add(b); + s.add(c); + s.add(d); + s.add(a); + s.add(cc); + s.add(dd); + s.add(e); + QCOMPARE(s.getEventsSpanning(6, 2), EventVector({ a, b, c, cc, d, dd })); + } + + void eventPatternWithin() { + + EventSeries s; + Event a(0, 1.0f, 18, QString("a")); + Event b(3, 2.0f, 6, QString("b")); + Event c(5, 3.0f, 2, QString("c")); + Event cc(5, 3.1f, 2, QString("cc")); + Event d(6, 4.0f, 10, QString("d")); + Event dd(6, 4.5f, 10, QString("dd")); + Event e(14, 5.0f, 3, QString("e")); + s.add(b); + s.add(c); + s.add(d); + s.add(a); + s.add(cc); + s.add(dd); + s.add(e); + QCOMPARE(s.getEventsWithin(2, 7), EventVector({ b, c, cc })); + } + + void eventPatternWithinWithOverspill() { + + EventSeries s; + Event a(0, 1.0f, 18, QString("a")); + Event b(3, 2.0f, 6, QString("b")); + Event c(5, 3.0f, 2, QString("c")); + Event cc(5, 3.1f, 2, QString("cc")); + Event d(6, 4.0f, 10, QString("d")); + Event dd(6, 4.5f, 10, QString("dd")); + Event e(14, 5.0f, 3, QString("e")); + s.add(b); + s.add(c); + s.add(d); + s.add(a); + s.add(cc); + s.add(dd); + s.add(e); + QCOMPARE(s.getEventsWithin(0, 0, 0), EventVector()); + QCOMPARE(s.getEventsWithin(0, 0, 1), EventVector({ a })); + QCOMPARE(s.getEventsWithin(0, 0, 2), EventVector({ a, b })); + QCOMPARE(s.getEventsWithin(20, 1, 0), EventVector()); + QCOMPARE(s.getEventsWithin(20, 1, 1), EventVector({ e })); + QCOMPARE(s.getEventsWithin(20, 1, 2), EventVector({ dd, e })); + QCOMPARE(s.getEventsWithin(2, 7, 0), EventVector({ b, c, cc })); + QCOMPARE(s.getEventsWithin(2, 7, 1), EventVector({ a, b, c, cc, d })); + QCOMPARE(s.getEventsWithin(2, 7, 2), EventVector({ a, b, c, cc, d, dd })); + QCOMPARE(s.getEventsWithin(2, 7, 3), EventVector({ a, b, c, cc, d, dd, e })); + QCOMPARE(s.getEventsWithin(2, 7, 4), EventVector({ a, b, c, cc, d, dd, e })); + } + + void eventPatternStartingWithin() { + + EventSeries s; + Event a(0, 1.0f, 18, QString("a")); + Event b(3, 2.0f, 6, QString("b")); + Event c(5, 3.0f, 2, QString("c")); + Event cc(5, 3.1f, 2, QString("cc")); + Event d(6, 4.0f, 10, QString("d")); + Event dd(6, 4.5f, 10, QString("dd")); + Event e(14, 5.0f, 3, QString("e")); + s.add(b); + s.add(c); + s.add(d); + s.add(a); + s.add(cc); + s.add(dd); + s.add(e); + QCOMPARE(s.getEventsStartingWithin(2, 7), + EventVector({ b, c, cc, d, dd })); + } + + void eventPatternStartingAt() { + + EventSeries s; + Event a(0, 1.0f, 18, QString("a")); + Event b(3, 2.0f, 6, QString("b")); + Event c(5, 3.0f, 2, QString("c")); + Event cc(5, 3.1f, 2, QString("cc")); + Event d(6, 4.0f, 10, QString("d")); + Event dd(6, 4.5f, 10, QString("dd")); + Event e(14, 5.0f, 3, QString("e")); + s.add(b); + s.add(c); + s.add(d); + s.add(a); + s.add(cc); + s.add(dd); + s.add(e); + QCOMPARE(s.getEventsStartingAt(2), EventVector()); + QCOMPARE(s.getEventsStartingAt(5), EventVector({ c, cc })); + } + + void eventPatternEndFrame() { + + EventSeries s; + Event a(0, 1.0f, 18, QString("a")); + Event b(3, 2.0f, 6, QString("b")); + Event c(5, 3.0f, 2, QString("c")); + Event cc(5, 3.1f, 2, QString("cc")); + Event d(6, 4.0f, 10, QString("d")); + Event dd(6, 4.5f, 10, QString("dd")); + Event e(14, 5.0f, 3, QString("e")); + s.add(b); + s.add(c); + s.add(d); + s.add(a); + s.add(cc); + s.add(dd); + s.add(e); + QCOMPARE(s.getEndFrame(), sv_frame_t(18)); + } + + void eventPatternAddRemove() { + + // This is mostly here to exercise the innards of EventSeries + // and check it doesn't crash out with any internal + // consistency problems + + EventSeries s; + Event a(0, 1.0f, 18, QString("a")); + Event b(3, 2.0f, 6, QString("b")); + Event c(5, 3.0f, 2, QString("c")); + Event cc(5, 3.1f, 2, QString("cc")); + Event d(6, 4.0f, 10, QString("d")); + Event dd(6, 4.5f, 10, QString("dd")); + Event e(14, 5.0f, 3, QString("e")); + s.add(b); + s.add(c); + s.add(d); + s.add(a); + s.add(cc); + s.add(dd); + s.add(e); + QCOMPARE(s.count(), 7); + s.remove(d); + QCOMPARE(s.getEventsCovering(8), EventVector({ a, b, dd })); + QCOMPARE(s.getEndFrame(), sv_frame_t(18)); + s.remove(e); + s.remove(a); + QCOMPARE(s.getEventsCovering(8), EventVector({ b, dd })); + QCOMPARE(s.getEndFrame(), sv_frame_t(16)); + s.remove(cc); + s.remove(c); + s.remove(dd); + QCOMPARE(s.getEventsCovering(8), EventVector({ b })); + QCOMPARE(s.getEndFrame(), sv_frame_t(9)); + s.remove(b); + QCOMPARE(s.getEventsCovering(8), EventVector()); + QCOMPARE(s.count(), 0); + QCOMPARE(s.isEmpty(), true); + QCOMPARE(s.getEndFrame(), sv_frame_t(0)); + } + + void preceding() { + + EventSeries s; + Event a(0, 1.0f, 18, QString("a")); + Event b(3, 2.0f, 6, QString("b")); + Event c(5, 3.0f, 2, QString("c")); + Event cc(5, 3.1f, 2, QString("cc")); + Event d(6, 4.0f, 10, QString("d")); + Event dd(6, 4.5f, 10, QString("dd")); + Event e(14, 5.0f, 3, QString("e")); + s.add(b); + s.add(c); + s.add(d); + s.add(d); // again + s.add(a); + s.add(cc); + s.add(dd); + s.add(e); + Event p; + QCOMPARE(s.getEventPreceding(e, p), true); + QCOMPARE(p, dd); + QCOMPARE(s.getEventPreceding(p, p), true); + QCOMPARE(p, d); + QCOMPARE(s.getEventPreceding(p, p), true); + QCOMPARE(p, cc); + QCOMPARE(s.getEventPreceding(p, p), true); + QCOMPARE(p, c); + QCOMPARE(s.getEventPreceding(p, p), true); + QCOMPARE(p, b); + QCOMPARE(s.getEventPreceding(p, p), true); + QCOMPARE(p, a); + QCOMPARE(s.getEventPreceding(p, p), false); + } + + void following() { + + EventSeries s; + Event a(0, 1.0f, 18, QString("a")); + Event b(3, 2.0f, 6, QString("b")); + Event c(5, 3.0f, 2, QString("c")); + Event cc(5, 3.1f, 2, QString("cc")); + Event d(6, 4.0f, 10, QString("d")); + Event dd(6, 4.5f, 10, QString("dd")); + Event e(14, 5.0f, 3, QString("e")); + s.add(b); + s.add(c); + s.add(d); + s.add(d); // again + s.add(a); + s.add(cc); + s.add(dd); + s.add(e); + Event p; + QCOMPARE(s.getEventFollowing(a, p), true); + QCOMPARE(p, b); + QCOMPARE(s.getEventFollowing(p, p), true); + QCOMPARE(p, c); + QCOMPARE(s.getEventFollowing(p, p), true); + QCOMPARE(p, cc); + QCOMPARE(s.getEventFollowing(p, p), true); + QCOMPARE(p, d); + QCOMPARE(s.getEventFollowing(p, p), true); + QCOMPARE(p, dd); + QCOMPARE(s.getEventFollowing(p, p), true); + QCOMPARE(p, e); + QCOMPARE(s.getEventFollowing(p, p), false); + } + + void matchingForward() { + + EventSeries s; + Event p; + QCOMPARE(s.getNearestEventMatching + (6, [](Event e) { return e.getDuration() < 4; }, + EventSeries::Forward, p), false); + Event a(0, 1.0f, 18, QString("a")); + Event b(3, 2.0f, 6, QString("b")); + Event c(5, 3.0f, 2, QString("c")); + Event cc(5, 3.1f, 2, QString("cc")); + Event d(6, 4.0f, 10, QString("d")); + Event dd(6, 4.5f, 10, QString("dd")); + Event e(14, 5.0f, 3, QString("e")); + s.add(b); + s.add(c); + s.add(d); + s.add(d); // again + s.add(a); + s.add(cc); + s.add(dd); + s.add(e); + QCOMPARE(s.getNearestEventMatching + (0, [](Event e) { return e.getDuration() < 4; }, + EventSeries::Forward, p), true); + QCOMPARE(p, c); + QCOMPARE(s.getNearestEventMatching + (6, [](Event e) { return e.getDuration() < 4; }, + EventSeries::Forward, p), true); + QCOMPARE(p, e); + QCOMPARE(s.getNearestEventMatching + (6, [](Event e) { return e.getDuration() > 4; }, + EventSeries::Forward, p), true); + QCOMPARE(p, d); + QCOMPARE(s.getNearestEventMatching + (20, [](Event e) { return e.getDuration() > 4; }, + EventSeries::Forward, p), false); + } + + void matchingBackward() { + + EventSeries s; + Event p; + QCOMPARE(s.getNearestEventMatching + (6, [](Event e) { return e.getDuration() < 4; }, + EventSeries::Backward, p), false); + Event a(0, 1.0f, 18, QString("a")); + Event b(3, 2.0f, 6, QString("b")); + Event c(5, 3.0f, 2, QString("c")); + Event cc(5, 3.1f, 2, QString("cc")); + Event d(6, 4.0f, 10, QString("d")); + Event dd(6, 4.5f, 10, QString("dd")); + Event e(14, 5.0f, 3, QString("e")); + s.add(b); + s.add(c); + s.add(d); + s.add(d); // again + s.add(a); + s.add(cc); + s.add(dd); + s.add(e); + QCOMPARE(s.getNearestEventMatching + (0, [](Event e) { return e.getDuration() < 4; }, + EventSeries::Backward, p), false); + QCOMPARE(s.getNearestEventMatching + (6, [](Event e) { return e.getDuration() > 4; }, + EventSeries::Backward, p), true); + QCOMPARE(p, b); + QCOMPARE(s.getNearestEventMatching + (20, [](Event e) { return e.getDuration() > 4; }, + EventSeries::Backward, p), true); + QCOMPARE(p, dd); + } +}; + +#endif diff -r ab4fd193262b -r 978c143c767f base/test/files.pri --- a/base/test/files.pri Thu May 16 12:54:58 2019 +0100 +++ b/base/test/files.pri Fri May 17 10:02:43 2019 +0100 @@ -4,10 +4,12 @@ TestMovingMedian.h \ TestOurRealTime.h \ TestPitch.h \ + TestEventSeries.h \ TestRangeMapper.h \ TestScaleTickIntervals.h \ TestStringBits.h \ - TestVampRealTime.h + TestVampRealTime.h \ + StressEventSeries.h TEST_SOURCES += \ svcore-base-test.cpp diff -r ab4fd193262b -r 978c143c767f base/test/svcore-base-test.cpp --- a/base/test/svcore-base-test.cpp Thu May 16 12:54:58 2019 +0100 +++ b/base/test/svcore-base-test.cpp Fri May 17 10:02:43 2019 +0100 @@ -20,6 +20,8 @@ #include "TestVampRealTime.h" #include "TestColumnOp.h" #include "TestMovingMedian.h" +#include "TestEventSeries.h" +#include "StressEventSeries.h" #include "system/Init.h" @@ -84,7 +86,19 @@ if (QTest::qExec(&t, argc, argv) == 0) ++good; else ++bad; } - + { + TestEventSeries t; + if (QTest::qExec(&t, argc, argv) == 0) ++good; + else ++bad; + } +/* + { + StressEventSeries t; + if (QTest::qExec(&t, argc, argv) == 0) ++good; + else ++bad; + } +*/ + if (bad > 0) { SVCERR << "\n********* " << bad << " test suite(s) failed!\n" << endl; return 1; diff -r ab4fd193262b -r 978c143c767f data/fileio/CSVFileReader.cpp --- a/data/fileio/CSVFileReader.cpp Thu May 16 12:54:58 2019 +0100 +++ b/data/fileio/CSVFileReader.cpp Fri May 17 10:02:43 2019 +0100 @@ -437,24 +437,24 @@ if (modelType == CSVFormat::OneDimensionalModel) { - SparseOneDimensionalModel::Point point(frameNo, label); - model1->addPoint(point); + Event point(frameNo, label); + model1->add(point); } else if (modelType == CSVFormat::TwoDimensionalModel) { - SparseTimeValueModel::Point point(frameNo, value, label); - model2->addPoint(point); + Event point(frameNo, value, label); + model2->add(point); } else if (modelType == CSVFormat::TwoDimensionalModelWithDuration) { - RegionModel::Point point(frameNo, value, duration, label); - model2a->addPoint(point); + Event region(frameNo, value, duration, label); + model2a->add(region); } else if (modelType == CSVFormat::TwoDimensionalModelWithDurationAndPitch) { float level = ((value >= 0.f && value <= 1.f) ? value : 1.f); - NoteModel::Point point(frameNo, pitch, duration, level, label); - model2b->addPoint(point); + Event note(frameNo, pitch, duration, level, label); + model2b->add(note); } else if (modelType == CSVFormat::ThreeDimensionalModel) { @@ -581,31 +581,30 @@ } } - map pointMap; - for (RegionModel::PointList::const_iterator i = - model2a->getPoints().begin(); - i != model2a->getPoints().end(); ++i) { - RegionModel::Point p(*i); - int count = labelCountMap[p.label]; - v = countLabelValueMap[count][p.label]; - // SVCERR << "mapping from label \"" << p.label << "\" (count " << count << ") to value " << v << endl; - RegionModel::Point pp(p.frame, v, p.duration, p.label); - pointMap[p] = pp; + map eventMap; + + EventVector allEvents = model2a->getAllEvents(); + for (const Event &e: allEvents) { + int count = labelCountMap[e.getLabel()]; + v = countLabelValueMap[count][e.getLabel()]; + // SVCERR << "mapping from label \"" << p.label + // << "\" (count " << count + // << ") to value " << v << endl; + eventMap[e] = Event(e.getFrame(), v, + e.getDuration(), e.getLabel()); } - for (map::iterator i = - pointMap.begin(); i != pointMap.end(); ++i) { + for (const auto &i: eventMap) { // There could be duplicate regions; if so replace // them all -- but we need to check we're not // replacing a region by itself (or else this will // never terminate) - if (i->first.value == i->second.value) { + if (i.first.getValue() == i.second.getValue()) { continue; } - while (model2a->containsPoint(i->first)) { - model2a->deletePoint(i->first); - model2a->addPoint(i->second); + while (model2a->containsEvent(i.first)) { + model2a->remove(i.first); + model2a->add(i.second); } } } diff -r ab4fd193262b -r 978c143c767f data/fileio/CSVStreamWriter.h --- a/data/fileio/CSVStreamWriter.h Thu May 16 12:54:58 2019 +0100 +++ b/data/fileio/CSVStreamWriter.h Fri May 17 10:02:43 2019 +0100 @@ -50,7 +50,6 @@ return acc + (current.getEndFrame() - current.getStartFrame()); } ); - const auto finalFrameOfLastRegion = (*selections.crbegin()).getEndFrame(); const auto wasCancelled = [&reporter]() { return reporter && reporter->wasCancelled(); @@ -58,6 +57,7 @@ sv_frame_t nFramesWritten = 0; int previousProgress = 0; + bool started = false; for (const auto& extents : selections) { const auto startFrame = extents.getStartFrame(); @@ -68,15 +68,20 @@ const auto start = readPtr; const auto end = std::min(start + blockSize, endFrame); - const auto data = model.toDelimitedDataStringSubsetWithOptions( + const auto data = model.toDelimitedDataString( delimiter, options, start, - end + end - start ).trimmed(); if ( data != "" ) { - oss << data << (end < finalFrameOfLastRegion ? "\n" : ""); + if (started) { + oss << "\n"; + } else { + started = true; + } + oss << data; } nFramesWritten += end - start; diff -r ab4fd193262b -r 978c143c767f data/fileio/MIDIFileReader.cpp --- a/data/fileio/MIDIFileReader.cpp Thu May 16 12:54:58 2019 +0100 +++ b/data/fileio/MIDIFileReader.cpp Fri May 17 10:02:43 2019 +0100 @@ -1027,12 +1027,12 @@ float level = float((*i)->getVelocity()) / 128.f; - Note note(startFrame, (*i)->getPitch(), - endFrame - startFrame, level, noteLabel); + Event note(startFrame, (*i)->getPitch(), + endFrame - startFrame, level, noteLabel); // SVDEBUG << "Adding note " << startFrame << "," << (endFrame-startFrame) << " : " << int((*i)->getPitch()) << endl; - model->addPoint(note); + model->add(note); break; } diff -r ab4fd193262b -r 978c143c767f data/fileio/MIDIFileWriter.cpp --- a/data/fileio/MIDIFileWriter.cpp Thu May 16 12:54:58 2019 +0100 +++ b/data/fileio/MIDIFileWriter.cpp Fri May 17 10:02:43 2019 +0100 @@ -23,8 +23,9 @@ #include "MIDIFileWriter.h" #include "data/midi/MIDIEvent.h" -#include "model/NoteData.h" +#include "base/NoteData.h" +#include "base/NoteExportable.h" #include "base/Pitch.h" #include diff -r ab4fd193262b -r 978c143c767f data/fileio/test/CSVStreamWriterTest.h --- a/data/fileio/test/CSVStreamWriterTest.h Thu May 16 12:54:58 2019 +0100 +++ b/data/fileio/test/CSVStreamWriterTest.h Fri May 17 10:02:43 2019 +0100 @@ -297,13 +297,15 @@ NoteModel notes(8 /* sampleRate */, 4 /* resolution */); sv_frame_t startFrame = 0; for (const auto& note : cMajorPentatonic) { - notes.addPoint({startFrame, note, 4, 1.f, ""}); + notes.add({startFrame, note, 4, 1.f, ""}); startFrame += 8; } // qDebug("Create Expected Output\n"); // NB. removed end line break - const auto expectedOutput = notes.toDelimitedDataString(",").trimmed(); + const auto expectedOutput = + notes.toDelimitedDataString(",", {}, 0, notes.getEndFrame()) + .trimmed(); StubReporter reporter { []() -> bool { return false; } }; std::ostringstream oss; @@ -318,9 +320,10 @@ 2 ); -// qDebug("\n%s\n", expectedOutput.toLocal8Bit().data()); -// qDebug("\n%s\n", oss.str().c_str()); +// qDebug("\n->>%s<<-\n", expectedOutput.toLocal8Bit().data()); +// qDebug("\n->>%s<<-\n", oss.str().c_str()); QVERIFY( wroteSparseModel == true ); + QVERIFY( oss.str() != std::string() ); QVERIFY( oss.str() == expectedOutput.toStdString() ); } }; diff -r ab4fd193262b -r 978c143c767f data/model/AggregateWaveModel.cpp --- a/data/model/AggregateWaveModel.cpp Thu May 16 12:54:58 2019 +0100 +++ b/data/model/AggregateWaveModel.cpp Fri May 17 10:02:43 2019 +0100 @@ -236,7 +236,7 @@ { QStringList componentStrings; for (const auto &c: m_components) { - componentStrings.push_back(QString("%1").arg(getObjectExportId(c.model))); + componentStrings.push_back(QString("%1").arg(c.model->getExportId())); } Model::toXml(out, indent, QString("type=\"aggregatewave\" components=\"%1\" %2") diff -r ab4fd193262b -r 978c143c767f data/model/AggregateWaveModel.h --- a/data/model/AggregateWaveModel.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/AggregateWaveModel.h Fri May 17 10:02:43 2019 +0100 @@ -41,6 +41,11 @@ bool isOK() const override; bool isReady(int *) const override; + int getCompletion() const override { + int c = 0; + (void)isReady(&c); + return c; + } QString getTypeName() const override { return tr("Aggregate Wave"); } diff -r ab4fd193262b -r 978c143c767f data/model/AlignmentModel.cpp --- a/data/model/AlignmentModel.cpp Thu May 16 12:54:58 2019 +0100 +++ b/data/model/AlignmentModel.cpp Fri May 17 10:02:43 2019 +0100 @@ -48,10 +48,21 @@ if (m_rawPath && m_rawPath->isReady()) { pathCompletionChanged(); } + + if (m_reference == m_aligned) { + // Trivial alignment, e.g. of main model to itself, which we + // record so that we can distinguish the reference model for + // alignments from an unaligned model. No path required + m_pathComplete = true; + } } AlignmentModel::~AlignmentModel() { +#ifdef DEBUG_ALIGNMENT_MODEL + SVCERR << "AlignmentModel(" << this << ")::~AlignmentModel()" << endl; +#endif + if (m_rawPath) m_rawPath->aboutToDelete(); delete m_rawPath; @@ -65,8 +76,9 @@ bool AlignmentModel::isOK() const { + if (m_error != "") return false; if (m_rawPath) return m_rawPath->isOK(); - else return true; + return true; } sv_frame_t @@ -97,14 +109,14 @@ if (!m_pathBegun && m_rawPath) { if (completion) *completion = 0; #ifdef DEBUG_ALIGNMENT_MODEL - SVDEBUG << "AlignmentModel::isReady: path not begun" << endl; + SVCERR << "AlignmentModel::isReady: path not begun" << endl; #endif return false; } if (m_pathComplete) { if (completion) *completion = 100; #ifdef DEBUG_ALIGNMENT_MODEL - SVDEBUG << "AlignmentModel::isReady: path complete" << endl; + SVCERR << "AlignmentModel::isReady: path complete" << endl; #endif return true; } @@ -114,7 +126,7 @@ // set at all yet (this case) if (completion) *completion = 0; #ifdef DEBUG_ALIGNMENT_MODEL - SVDEBUG << "AlignmentModel::isReady: no raw path" << endl; + SVCERR << "AlignmentModel::isReady: no raw path" << endl; #endif return false; } @@ -196,8 +208,8 @@ m_rawPath->isReady(&completion); #ifdef DEBUG_ALIGNMENT_MODEL - cerr << "AlignmentModel::pathCompletionChanged: completion = " - << completion << endl; + SVCERR << "AlignmentModel::pathCompletionChanged: completion = " + << completion << endl; #endif m_pathComplete = (completion == 100); @@ -206,7 +218,10 @@ constructPath(); constructReversePath(); - + +#ifdef DEBUG_ALIGNMENT_MODEL + SVCERR << "AlignmentModel: path complete" << endl; +#endif } } @@ -230,14 +245,13 @@ m_path->clear(); - SparseTimeValueModel::PointList points = m_rawPath->getPoints(); - - for (SparseTimeValueModel::PointList::const_iterator i = points.begin(); - i != points.end(); ++i) { - sv_frame_t frame = i->frame; - double value = i->value; + EventVector points = m_rawPath->getAllEvents(); + + for (const auto &p: points) { + sv_frame_t frame = p.getFrame(); + double value = p.getValue(); sv_frame_t rframe = lrint(value * m_aligned->getSampleRate()); - m_path->addPoint(PathPoint(frame, rframe)); + m_path->add(PathPoint(frame, rframe)); } #ifdef DEBUG_ALIGNMENT_MODEL @@ -268,7 +282,7 @@ i != points.end(); ++i) { sv_frame_t frame = i->frame; sv_frame_t rframe = i->mapframe; - m_reversePath->addPoint(PathPoint(rframe, frame)); + m_reversePath->add(PathPoint(rframe, frame)); } #ifdef DEBUG_ALIGNMENT_MODEL @@ -299,7 +313,7 @@ cerr << "AlignmentModel::align: frame " << frame << " requested" << endl; #endif - PathModel::Point point(frame); + PathPoint point(frame); PathModel::PointList::const_iterator i = points.lower_bound(point); if (i == points.end()) { #ifdef DEBUG_ALIGNMENT_MODEL @@ -359,6 +373,10 @@ m_rawPath = rawpath; + if (!m_rawPath) { + return; + } + connect(m_rawPath, SIGNAL(modelChanged()), this, SLOT(pathChanged())); @@ -407,9 +425,9 @@ Model::toXml(stream, indent, QString("type=\"alignment\" reference=\"%1\" aligned=\"%2\" path=\"%3\" %4") - .arg(getObjectExportId(m_reference)) - .arg(getObjectExportId(m_aligned)) - .arg(getObjectExportId(m_path)) + .arg(m_reference->getExportId()) + .arg(m_aligned->getExportId()) + .arg(m_path->getExportId()) .arg(extraAttributes)); } diff -r ab4fd193262b -r 978c143c767f data/model/AlignmentModel.h --- a/data/model/AlignmentModel.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/AlignmentModel.h Fri May 17 10:02:43 2019 +0100 @@ -36,10 +36,19 @@ ~AlignmentModel(); bool isOK() const override; + + void setError(QString error) { m_error = error; } + QString getError() const { return m_error; } + sv_frame_t getStartFrame() const override; sv_frame_t getEndFrame() const override; sv_samplerate_t getSampleRate() const override; bool isReady(int *completion = 0) const override; + int getCompletion() const override { + int c = 0; + (void)isReady(&c); + return c; + } const ZoomConstraint *getZoomConstraint() const override; QString getTypeName() const override { return tr("Alignment"); } @@ -57,6 +66,11 @@ QString indent = "", QString extraAttributes = "") const override; + QString toDelimitedDataString(QString, DataExportOptions, + sv_frame_t, sv_frame_t) const override { + return ""; + } + signals: void modelChanged(); void modelChangedWithin(sv_frame_t startFrame, sv_frame_t endFrame); @@ -76,6 +90,7 @@ mutable PathModel *m_reversePath; // I own this bool m_pathBegun; bool m_pathComplete; + QString m_error; void constructPath() const; void constructReversePath() const; diff -r ab4fd193262b -r 978c143c767f data/model/DeferredNotifier.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/data/model/DeferredNotifier.h Fri May 17 10:02:43 2019 +0100 @@ -0,0 +1,76 @@ +/* -*- 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_DEFERRED_NOTIFIER_H +#define SV_DEFERRED_NOTIFIER_H + +#include "Model.h" + +#include "base/Extents.h" + +#include +#include + +class DeferredNotifier +{ +public: + enum Mode { + NOTIFY_ALWAYS, + NOTIFY_DEFERRED + }; + + DeferredNotifier(Model *m, Mode mode) : m_model(m), m_mode(mode) { } + + Mode getMode() const { + return m_mode; + } + void switchMode(Mode newMode) { + m_mode = newMode; + } + + void update(sv_frame_t frame, sv_frame_t duration) { + if (m_mode == NOTIFY_ALWAYS) { + m_model->modelChangedWithin(frame, frame + duration); + } else { + QMutexLocker locker(&m_mutex); + m_extents.sample(frame); + m_extents.sample(frame + duration); + } + } + + void makeDeferredNotifications() { + bool shouldEmit = false; + sv_frame_t from, to; + { QMutexLocker locker(&m_mutex); + if (m_extents.isSet()) { + shouldEmit = true; + from = m_extents.getMin(); + to = m_extents.getMax(); + } + } + if (shouldEmit) { + m_model->modelChangedWithin(from, to); + QMutexLocker locker(&m_mutex); + m_extents.reset(); + } + } + +private: + Model *m_model; + Mode m_mode; + QMutex m_mutex; + Extents m_extents; +}; + +#endif diff -r ab4fd193262b -r 978c143c767f data/model/Dense3DModelPeakCache.h --- a/data/model/Dense3DModelPeakCache.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/Dense3DModelPeakCache.h Fri May 17 10:02:43 2019 +0100 @@ -97,6 +97,11 @@ return m_source->getCompletion(); } + QString toDelimitedDataString(QString, DataExportOptions, + sv_frame_t, sv_frame_t) const override { + return ""; + } + protected slots: void sourceModelChanged(); void sourceModelAboutToBeDeleted(); diff -r ab4fd193262b -r 978c143c767f data/model/DenseThreeDimensionalModel.h --- a/data/model/DenseThreeDimensionalModel.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/DenseThreeDimensionalModel.h Fri May 17 10:02:43 2019 +0100 @@ -120,7 +120,7 @@ QString getTypeName() const override { return tr("Dense 3-D"); } - virtual int getCompletion() const = 0; + virtual int getCompletion() const override = 0; /* TabularModel methods. diff -r ab4fd193262b -r 978c143c767f data/model/DenseTimeValueModel.cpp --- a/data/model/DenseTimeValueModel.cpp Thu May 16 12:54:58 2019 +0100 +++ b/data/model/DenseTimeValueModel.cpp Fri May 17 10:02:43 2019 +0100 @@ -29,22 +29,23 @@ } QString -DenseTimeValueModel::toDelimitedDataStringSubset(QString delimiter, sv_frame_t f0, sv_frame_t f1) const +DenseTimeValueModel::toDelimitedDataString(QString delimiter, + DataExportOptions, + sv_frame_t startFrame, + sv_frame_t duration) const { int ch = getChannelCount(); -// cerr << "f0 = " << f0 << ", f1 = " << f1 << endl; + if (duration <= 0) return ""; - if (f1 <= f0) return ""; - - auto data = getMultiChannelData(0, ch - 1, f0, f1 - f0); + auto data = getMultiChannelData(0, ch - 1, startFrame, duration); if (data.empty() || data[0].empty()) return ""; QStringList list; for (sv_frame_t i = 0; in_range_for(data[0], i); ++i) { QStringList parts; - parts << QString("%1").arg(f0 + i); + parts << QString("%1").arg(startFrame + i); for (int c = 0; in_range_for(data, c); ++c) { parts << QString("%1").arg(data[c][i]); } diff -r ab4fd193262b -r 978c143c767f data/model/DenseTimeValueModel.h --- a/data/model/DenseTimeValueModel.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/DenseTimeValueModel.h Fri May 17 10:02:43 2019 +0100 @@ -82,8 +82,10 @@ bool canPlay() const override { return true; } QString getDefaultPlayClipId() const override { return ""; } - QString toDelimitedDataStringSubset(QString delimiter, - sv_frame_t f0, sv_frame_t f1) const override; + QString toDelimitedDataString(QString delimiter, + DataExportOptions options, + sv_frame_t startFrame, + sv_frame_t duration) const override; QString getTypeName() const override { return tr("Dense Time-Value"); } }; diff -r ab4fd193262b -r 978c143c767f data/model/EditableDenseThreeDimensionalModel.cpp --- a/data/model/EditableDenseThreeDimensionalModel.cpp Thu May 16 12:54:58 2019 +0100 +++ b/data/model/EditableDenseThreeDimensionalModel.cpp Fri May 17 10:02:43 2019 +0100 @@ -57,6 +57,13 @@ return true; } +bool +EditableDenseThreeDimensionalModel::isReady(int *completion) const +{ + if (completion) *completion = getCompletion(); + return true; +} + sv_samplerate_t EditableDenseThreeDimensionalModel::getSampleRate() const { @@ -486,29 +493,23 @@ } } -QString -EditableDenseThreeDimensionalModel::toDelimitedDataString(QString delimiter) const +int +EditableDenseThreeDimensionalModel::getCompletion() const { - QReadLocker locker(&m_lock); - QString s; - for (int i = 0; in_range_for(m_data, i); ++i) { - QStringList list; - for (int j = 0; in_range_for(m_data.at(i), j); ++j) { - list << QString("%1").arg(m_data.at(i).at(j)); - } - s += list.join(delimiter) + "\n"; - } - return s; + return m_completion; } QString -EditableDenseThreeDimensionalModel::toDelimitedDataStringSubset(QString delimiter, sv_frame_t f0, sv_frame_t f1) const +EditableDenseThreeDimensionalModel::toDelimitedDataString(QString delimiter, + DataExportOptions, + sv_frame_t startFrame, + sv_frame_t duration) const { QReadLocker locker(&m_lock); QString s; for (int i = 0; in_range_for(m_data, i); ++i) { sv_frame_t fr = m_startFrame + i * m_resolution; - if (fr >= f0 && fr < f1) { + if (fr >= startFrame && fr < startFrame + duration) { QStringList list; for (int j = 0; in_range_for(m_data.at(i), j); ++j) { list << QString("%1").arg(m_data.at(i).at(j)); @@ -526,7 +527,11 @@ { QReadLocker locker(&m_lock); - // For historical reasons we read and write "resolution" as "windowSize" + // For historical reasons we read and write "resolution" as "windowSize". + + // Our dataset doesn't have its own export ID, we just use + // ours. Actually any model could do that, since datasets aren't + // in the same id-space as models when re-read SVDEBUG << "EditableDenseThreeDimensionalModel::toXml" << endl; @@ -537,13 +542,13 @@ .arg(m_yBinCount) .arg(m_minimum) .arg(m_maximum) - .arg(getObjectExportId(&m_data)) + .arg(getExportId()) .arg(m_startFrame) .arg(extraAttributes)); out << indent; out << QString("\n") - .arg(getObjectExportId(&m_data)); + .arg(getExportId()); for (int i = 0; i < (int)m_binNames.size(); ++i) { if (m_binNames[i] != "") { diff -r ab4fd193262b -r 978c143c767f data/model/EditableDenseThreeDimensionalModel.h --- a/data/model/EditableDenseThreeDimensionalModel.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/EditableDenseThreeDimensionalModel.h Fri May 17 10:02:43 2019 +0100 @@ -49,6 +49,9 @@ bool notifyOnAdd = true); bool isOK() const override; + bool isReady(int *completion = 0) const override; + void setCompletion(int completion, bool update = true); + int getCompletion() const override; sv_samplerate_t getSampleRate() const override; sv_frame_t getStartFrame() const override; @@ -183,13 +186,12 @@ */ bool shouldUseLogValueScale() const override; - virtual void setCompletion(int completion, bool update = true); - int getCompletion() const override { return m_completion; } - QString getTypeName() const override { return tr("Editable Dense 3-D"); } - QString toDelimitedDataString(QString delimiter) const override; - QString toDelimitedDataStringSubset(QString delimiter, sv_frame_t f0, sv_frame_t f1) const override; + QString toDelimitedDataString(QString delimiter, + DataExportOptions options, + sv_frame_t startFrame, + sv_frame_t duration) const override; void toXml(QTextStream &out, QString indent = "", diff -r ab4fd193262b -r 978c143c767f data/model/EventCommands.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/data/model/EventCommands.h Fri May 17 10:02:43 2019 +0100 @@ -0,0 +1,140 @@ +/* -*- 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_COMMANDS_H +#define SV_EVENT_COMMANDS_H + +#include "base/Event.h" +#include "base/Command.h" + +/** + * Interface for classes that can be modified through these commands + */ +class EventEditable +{ +public: + virtual void add(Event e) = 0; + virtual void remove(Event e) = 0; +}; + +/** + * Command to add an event to an editable containing events, with undo. + */ +class AddEventCommand : public Command +{ +public: + AddEventCommand(EventEditable *editable, const Event &e, QString name) : + m_editable(editable), m_event(e), m_name(name) { } + + QString getName() const override { return m_name; } + Event getEvent() const { return m_event; } + + void execute() override { m_editable->add(m_event); } + void unexecute() override { m_editable->remove(m_event); } + +private: + EventEditable *m_editable; + Event m_event; + QString m_name; +}; + +/** + * Command to remove an event from an editable containing events, with + * undo. + */ +class RemoveEventCommand : public Command +{ +public: + RemoveEventCommand(EventEditable *editable, const Event &e, QString name) : + m_editable(editable), m_event(e), m_name(name) { } + + QString getName() const override { return m_name; } + Event getEvent() const { return m_event; } + + void execute() override { m_editable->remove(m_event); } + void unexecute() override { m_editable->add(m_event); } + +private: + EventEditable *m_editable; + Event m_event; + QString m_name; +}; + +/** + * Command to add or remove a series of events to or from an editable, + * with undo. Creates and immediately executes a sub-command for each + * add/remove requested. Consecutive add/remove pairs for the same + * point are collapsed. + */ +class ChangeEventsCommand : public MacroCommand +{ +public: + ChangeEventsCommand(EventEditable *editable, QString name) : + MacroCommand(name), m_editable(editable) { } + + void add(Event e) { + addCommand(new AddEventCommand(m_editable, e, getName()), true); + } + void remove(Event e) { + addCommand(new RemoveEventCommand(m_editable, e, getName()), true); + } + + /** + * Stack an arbitrary other command in the same sequence. + */ + void addCommand(Command *command) override { addCommand(command, true); } + + /** + * If any points have been added or deleted, return this + * command (so the caller can add it to the command history). + * Otherwise delete the command and return NULL. + */ + ChangeEventsCommand *finish() { + if (!m_commands.empty()) { + return this; + } else { + delete this; + return nullptr; + } + } + +protected: + virtual void addCommand(Command *command, bool executeFirst) { + + if (executeFirst) command->execute(); + + if (m_commands.empty()) { + MacroCommand::addCommand(command); + return; + } + + RemoveEventCommand *r = + dynamic_cast(command); + AddEventCommand *a = + dynamic_cast(*m_commands.rbegin()); + if (r && a) { + if (a->getEvent() == r->getEvent()) { + deleteCommand(a); + return; + } + } + + MacroCommand::addCommand(command); + } + + EventEditable *m_editable; +}; + +#endif diff -r ab4fd193262b -r 978c143c767f data/model/FFTModel.cpp --- a/data/model/FFTModel.cpp Thu May 16 12:54:58 2019 +0100 +++ b/data/model/FFTModel.cpp Fri May 17 10:02:43 2019 +0100 @@ -55,8 +55,8 @@ if (m_windowSize > m_fftSize) { SVCERR << "ERROR: FFTModel::FFTModel: window size (" << m_windowSize - << ") must be at least FFT size (" << m_fftSize << ")" << endl; - throw invalid_argument("FFTModel window size must be at least FFT size"); + << ") may not exceed FFT size (" << m_fftSize << ")" << endl; + throw invalid_argument("FFTModel window size may not exceed FFT size"); } m_fft.initFloat(); diff -r ab4fd193262b -r 978c143c767f data/model/FFTModel.h --- a/data/model/FFTModel.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/FFTModel.h Fri May 17 10:02:43 2019 +0100 @@ -88,6 +88,10 @@ } virtual QString getError() const { return ""; } //!!!??? virtual sv_frame_t getFillExtent() const { return getEndFrame(); } + QString toDelimitedDataString(QString, DataExportOptions, + sv_frame_t, sv_frame_t) const override { + return ""; + } // FFTModel methods: // diff -r ab4fd193262b -r 978c143c767f data/model/FlexiNoteModel.h --- a/data/model/FlexiNoteModel.h Thu May 16 12:54:58 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,268 +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_FLEXINOTE_MODEL_H -#define SV_FLEXINOTE_MODEL_H - -#include "IntervalModel.h" -#include "NoteData.h" -#include "base/RealTime.h" -#include "base/Pitch.h" -#include "base/PlayParameterRepository.h" - -/** - * FlexiNoteModel -- a concrete IntervalModel for notes. - */ - -/** - * Extension of the NoteModel for more flexible note interaction. - * The original NoteModel rationale is given below, will need to be - * updated for FlexiNoteModel: - * - * Note type for use in a sparse model. All we mean by a "note" is - * something that has an onset time, a single value, a duration, and a - * level. Like other points, it can also have a label. With this - * point type, the model can be thought of as representing a simple - * MIDI-type piano roll, except that the y coordinates (values) do not - * have to be discrete integers. - */ - -struct FlexiNote -{ -public: - FlexiNote(sv_frame_t _frame) : frame(_frame), value(0.0f), duration(0), level(1.f) { } - FlexiNote(sv_frame_t _frame, float _value, sv_frame_t _duration, float _level, QString _label) : - frame(_frame), value(_value), duration(_duration), level(_level), label(_label) { } - - int getDimensions() const { return 3; } - - sv_frame_t frame; - float value; - sv_frame_t duration; - float level; - QString label; - - QString getLabel() const { return label; } - - void toXml(QTextStream &stream, - QString indent = "", - QString extraAttributes = "") const - { - stream << - QString("%1\n") - .arg(indent).arg(frame).arg(value).arg(duration).arg(level) - .arg(XmlExportable::encodeEntities(label)).arg(extraAttributes); - } - - QString toDelimitedDataString(QString delimiter, DataExportOptions opts, sv_samplerate_t sampleRate) const - { - QStringList list; - list << RealTime::frame2RealTime(frame, sampleRate).toString().c_str(); - list << QString("%1").arg(value); - list << RealTime::frame2RealTime(duration, sampleRate).toString().c_str(); - if (!(opts & DataExportOmitLevels)) { - list << QString("%1").arg(level); - } - if (label != "") list << label; - return list.join(delimiter); - } - - struct Comparator { - bool operator()(const FlexiNote &p1, - const FlexiNote &p2) const { - if (p1.frame != p2.frame) return p1.frame < p2.frame; - if (p1.value != p2.value) return p1.value < p2.value; - if (p1.duration != p2.duration) return p1.duration < p2.duration; - if (p1.level != p2.level) return p1.level < p2.level; - return p1.label < p2.label; - } - }; - - struct OrderComparator { - bool operator()(const FlexiNote &p1, - const FlexiNote &p2) const { - return p1.frame < p2.frame; - } - }; -}; - - -class FlexiNoteModel : public IntervalModel, public NoteExportable -{ - Q_OBJECT - -public: - FlexiNoteModel(sv_samplerate_t sampleRate, int resolution, - bool notifyOnAdd = true) : - IntervalModel(sampleRate, resolution, notifyOnAdd), - m_valueQuantization(0) - { - PlayParameterRepository::getInstance()->addPlayable(this); - } - - FlexiNoteModel(sv_samplerate_t sampleRate, int resolution, - float valueMinimum, float valueMaximum, - bool notifyOnAdd = true) : - IntervalModel(sampleRate, resolution, - valueMinimum, valueMaximum, - notifyOnAdd), - m_valueQuantization(0) - { - PlayParameterRepository::getInstance()->addPlayable(this); - } - - virtual ~FlexiNoteModel() - { - PlayParameterRepository::getInstance()->removePlayable(this); - } - - float getValueQuantization() const { return m_valueQuantization; } - void setValueQuantization(float q) { m_valueQuantization = q; } - float getValueMinimum() const override { return 33; } - float getValueMaximum() const override { return 88; } - - QString getTypeName() const override { return tr("FlexiNote"); } - - bool canPlay() const override { return true; } - - QString getDefaultPlayClipId() const override - { - return "elecpiano"; - } - - void toXml(QTextStream &out, - QString indent = "", - QString extraAttributes = "") const override - { - std::cerr << "FlexiNoteModel::toXml: extraAttributes = \"" - << extraAttributes.toStdString() << std::endl; - - IntervalModel::toXml - (out, - indent, - QString("%1 subtype=\"flexinote\" valueQuantization=\"%2\"") - .arg(extraAttributes).arg(m_valueQuantization)); - } - - /** - * TabularModel methods. - */ - - int getColumnCount() const override - { - return 6; - } - - QString getHeading(int column) const override - { - switch (column) { - case 0: return tr("Time"); - case 1: return tr("Frame"); - case 2: return tr("Pitch"); - case 3: return tr("Duration"); - case 4: return tr("Level"); - case 5: return tr("Label"); - default: return tr("Unknown"); - } - } - - QVariant getData(int row, int column, int role) const override - { - if (column < 4) { - return IntervalModel::getData(row, column, role); - } - - PointListConstIterator i = getPointListIteratorForRow(row); - if (i == m_points.end()) return QVariant(); - - switch (column) { - case 4: return i->level; - case 5: return i->label; - default: return QVariant(); - } - } - - Command *getSetDataCommand(int row, int column, const QVariant &value, int role) override - { - if (column < 4) { - return IntervalModel::getSetDataCommand - (row, column, value, role); - } - - if (role != Qt::EditRole) return 0; - PointListConstIterator i = getPointListIteratorForRow(row); - if (i == m_points.end()) return 0; - EditCommand *command = new EditCommand(this, tr("Edit Data")); - - Point point(*i); - command->deletePoint(point); - - switch (column) { - case 4: point.level = float(value.toDouble()); break; - case 5: point.label = value.toString(); break; - } - - command->addPoint(point); - return command->finish(); - } - - SortType getSortType(int column) const override - { - if (column == 5) return SortAlphabetical; - return SortNumeric; - } - - /** - * NoteExportable methods. - */ - - NoteList getNotes() const - override { - return getNotesWithin(getStartFrame(), getEndFrame()); - } - - NoteList getNotesWithin(sv_frame_t startFrame, sv_frame_t endFrame) const - override { - PointList points = getPoints(startFrame, endFrame); - NoteList notes; - for (PointList::iterator pli = points.begin(); pli != points.end(); ++pli) { - sv_frame_t duration = pli->duration; - if (duration == 0 || duration == 1) { - duration = sv_frame_t(getSampleRate() / 20); - } - int pitch = int(lrintf(pli->value)); - - int velocity = 100; - if (pli->level > 0.f && pli->level <= 1.f) { - velocity = int(lrintf(pli->level * 127)); - } - - NoteData note(pli->frame, duration, pitch, velocity); - - if (getScaleUnits() == "Hz") { - note.frequency = pli->value; - note.midiPitch = Pitch::getPitchForFrequency(note.frequency); - note.isMidiPitchQuantized = false; - } - notes.push_back(note); - } - return notes; - } - -protected: - float m_valueQuantization; -}; - -#endif diff -r ab4fd193262b -r 978c143c767f data/model/ImageModel.h --- a/data/model/ImageModel.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/ImageModel.h Fri May 17 10:02:43 2019 +0100 @@ -16,138 +16,174 @@ #ifndef SV_IMAGE_MODEL_H #define SV_IMAGE_MODEL_H -#include "SparseModel.h" +#include "EventCommands.h" +#include "TabularModel.h" +#include "Model.h" +#include "DeferredNotifier.h" + +#include "base/EventSeries.h" #include "base/XmlExportable.h" #include "base/RealTime.h" +#include "system/System.h" + #include /** - * Image point type for use in a SparseModel. This represents an - * image, identified by filename, at a given time. The filename can - * be empty, in which case we instead have a space to put an image in. + * A model representing image annotations, identified by filename or + * URI, at a given time, with an optional label. The filename can be + * empty, in which case we instead have a space to put an image in. */ -struct ImagePoint : public XmlExportable -{ -public: - ImagePoint(sv_frame_t _frame) : frame(_frame) { } - ImagePoint(sv_frame_t _frame, QString _image, QString _label) : - frame(_frame), image(_image), label(_label) { } - - int getDimensions() const { return 1; } - - sv_frame_t frame; - QString image; - QString label; - - QString getLabel() const { return label; } - - void toXml(QTextStream &stream, - QString indent = "", - QString extraAttributes = "") const override - { - stream << - QString("%1\n") - .arg(indent).arg(frame) - .arg(encodeEntities(image)) - .arg(encodeEntities(label)) - .arg(extraAttributes); - } - - QString toDelimitedDataString(QString delimiter, DataExportOptions, sv_samplerate_t sampleRate) const - { - QStringList list; - list << RealTime::frame2RealTime(frame, sampleRate).toString().c_str(); - list << image; - if (label != "") list << label; - return list.join(delimiter); - } - - struct Comparator { - bool operator()(const ImagePoint &p1, - const ImagePoint &p2) const { - if (p1.frame != p2.frame) return p1.frame < p2.frame; - if (p1.label != p2.label) return p1.label < p2.label; - return p1.image < p2.image; - } - }; - - struct OrderComparator { - bool operator()(const ImagePoint &p1, - const ImagePoint &p2) const { - return p1.frame < p2.frame; - } - }; -}; - - -// Make this a class rather than a typedef so it can be predeclared. - -class ImageModel : public SparseModel +class ImageModel : public Model, + public TabularModel, + public EventEditable { Q_OBJECT public: - ImageModel(sv_samplerate_t sampleRate, int resolution, bool notifyOnAdd = true) : - SparseModel(sampleRate, resolution, notifyOnAdd) - { } + ImageModel(sv_samplerate_t sampleRate, + int resolution, + bool notifyOnAdd = true) : + m_sampleRate(sampleRate), + m_resolution(resolution), + m_notifier(this, + notifyOnAdd ? + DeferredNotifier::NOTIFY_ALWAYS : + DeferredNotifier::NOTIFY_DEFERRED), + m_completion(100) { + } QString getTypeName() const override { return tr("Image"); } + bool isSparse() const override { return true; } + bool isOK() const override { return true; } - void toXml(QTextStream &out, - QString indent = "", - QString extraAttributes = "") const override - { - SparseModel::toXml - (out, - indent, - QString("%1 subtype=\"image\"") - .arg(extraAttributes)); + sv_frame_t getStartFrame() const override { + return m_events.getStartFrame(); + } + sv_frame_t getEndFrame() const override { + if (m_events.isEmpty()) return 0; + sv_frame_t e = m_events.getEndFrame() + 1; + if (e % m_resolution == 0) return e; + else return (e / m_resolution + 1) * m_resolution; + } + + sv_samplerate_t getSampleRate() const override { return m_sampleRate; } + int getResolution() const { return m_resolution; } + + int getCompletion() const override { return m_completion; } + + void setCompletion(int completion, bool update = true) { + + { QMutexLocker locker(&m_mutex); + if (m_completion == completion) return; + m_completion = completion; + } + + if (update) { + m_notifier.makeDeferredNotifications(); + } + + emit completionChanged(); + + if (completion == 100) { + // henceforth: + m_notifier.switchMode(DeferredNotifier::NOTIFY_ALWAYS); + emit modelChanged(); + } + } + + /** + * Query methods. + */ + + //!!! todo: go through all models, weeding out the methods here + //!!! that are not used + + int getEventCount() const { + return m_events.count(); + } + bool isEmpty() const { + return m_events.isEmpty(); + } + bool containsEvent(const Event &e) const { + return m_events.contains(e); + } + EventVector getAllEvents() const { + return m_events.getAllEvents(); + } + EventVector getEventsSpanning(sv_frame_t f, sv_frame_t duration) const { + return m_events.getEventsSpanning(f, duration); + } + EventVector getEventsCovering(sv_frame_t f) const { + return m_events.getEventsCovering(f); + } + EventVector getEventsWithin(sv_frame_t f, sv_frame_t duration, + int overspill = 0) const { + return m_events.getEventsWithin(f, duration, overspill); + } + EventVector getEventsStartingWithin(sv_frame_t f, sv_frame_t duration) const { + return m_events.getEventsStartingWithin(f, duration); + } + EventVector getEventsStartingAt(sv_frame_t f) const { + return m_events.getEventsStartingAt(f); + } + bool getNearestEventMatching(sv_frame_t startSearchAt, + std::function predicate, + EventSeries::Direction direction, + Event &found) const { + return m_events.getNearestEventMatching + (startSearchAt, predicate, direction, found); } /** - * Command to change the image for a point. + * Editing methods. */ - class ChangeImageCommand : public Command - { - public: - ChangeImageCommand(ImageModel *model, - const ImagePoint &point, - QString newImage, - QString newLabel) : - m_model(model), m_oldPoint(point), m_newPoint(point) { - m_newPoint.image = newImage; - m_newPoint.label = newLabel; + void add(Event e) override { + + { QMutexLocker locker(&m_mutex); + m_events.add(e.withoutDuration().withoutValue().withoutLevel()); } - - QString getName() const override { return tr("Edit Image"); } - - void execute() override { - m_model->deletePoint(m_oldPoint); - m_model->addPoint(m_newPoint); - std::swap(m_oldPoint, m_newPoint); + + m_notifier.update(e.getFrame(), m_resolution); + } + + void remove(Event e) override { + { QMutexLocker locker(&m_mutex); + m_events.remove(e); } - - void unexecute() override { execute(); } - - private: - ImageModel *m_model; - ImagePoint m_oldPoint; - ImagePoint m_newPoint; - }; + emit modelChangedWithin(e.getFrame(), e.getFrame() + m_resolution); + } /** * TabularModel methods. */ - int getColumnCount() const override - { + int getRowCount() const override { + return m_events.count(); + } + + int getColumnCount() const override { return 4; } - QString getHeading(int column) const override - { + bool isColumnTimeValue(int column) const override { + return (column < 2); + } + + sv_frame_t getFrameForRow(int row) const override { + if (row < 0 || row >= m_events.count()) { + return 0; + } + Event e = m_events.getEventByIndex(row); + return e.getFrame(); + } + + int getRowForFrame(sv_frame_t frame) const override { + return m_events.getIndexForEvent(Event(frame)); + } + + QString getHeading(int column) const override { switch (column) { case 0: return tr("Time"); case 1: return tr("Frame"); @@ -157,61 +193,100 @@ } } - QVariant getData(int row, int column, int role) const override - { - if (column < 2) { - return SparseModel::getData - (row, column, role); + SortType getSortType(int column) const override { + if (column >= 2) return SortAlphabetical; + return SortNumeric; + } + + QVariant getData(int row, int column, int role) const override { + + if (row < 0 || row >= m_events.count()) { + return QVariant(); } - PointListConstIterator i = getPointListIteratorForRow(row); - if (i == m_points.end()) return QVariant(); + Event e = m_events.getEventByIndex(row); switch (column) { - case 2: return i->image; - case 3: return i->label; + case 0: return adaptFrameForRole(e.getFrame(), getSampleRate(), role); + case 1: return int(e.getFrame()); + case 2: return e.getURI(); + case 3: return e.getLabel(); default: return QVariant(); } } - Command *getSetDataCommand(int row, int column, const QVariant &value, int role) override - { - if (column < 2) { - return SparseModel::getSetDataCommand - (row, column, value, role); + Command *getSetDataCommand(int row, int column, const QVariant &value, int role) override { + + if (row < 0 || row >= m_events.count()) return nullptr; + if (role != Qt::EditRole) return nullptr; + + Event e0 = m_events.getEventByIndex(row); + Event e1; + + switch (column) { + case 0: e1 = e0.withFrame(sv_frame_t(round(value.toDouble() * + getSampleRate()))); break; + case 1: e1 = e0.withFrame(value.toInt()); break; + case 2: e1 = e0.withURI(value.toString()); break; + case 3: e1 = e0.withLabel(value.toString()); break; } - if (role != Qt::EditRole) return 0; - PointListIterator i = getPointListIteratorForRow(row); - if (i == m_points.end()) return 0; - EditCommand *command = new EditCommand(this, tr("Edit Data")); - - Point point(*i); - command->deletePoint(point); - - switch (column) { - case 2: point.image = value.toString(); break; - case 3: point.label = value.toString(); break; - } - - command->addPoint(point); + ChangeEventsCommand *command = + new ChangeEventsCommand(this, tr("Edit Data")); + command->remove(e0); + command->add(e1); return command->finish(); } + + /** + * XmlExportable methods. + */ + void toXml(QTextStream &out, + QString indent = "", + QString extraAttributes = "") const override { - bool isColumnTimeValue(int column) const override - { - return (column < 2); + Model::toXml + (out, + indent, + QString("type=\"sparse\" dimensions=\"1\" resolution=\"%1\" " + "notifyOnAdd=\"%2\" dataset=\"%3\" subtype=\"image\" %4") + .arg(m_resolution) + .arg("true") // always true after model reaches 100% - + // subsequent events are always notified + .arg(m_events.getExportId()) + .arg(extraAttributes)); + + Event::ExportNameOptions options; + options.uriAttributeName = "image"; + + m_events.toXml(out, indent, QString("dimensions=\"1\""), options); } - SortType getSortType(int column) const override - { - if (column > 2) return SortAlphabetical; - return SortNumeric; + QString toDelimitedDataString(QString delimiter, + DataExportOptions options, + sv_frame_t startFrame, + sv_frame_t duration) const override { + return m_events.toDelimitedDataString(delimiter, + options, + startFrame, + duration, + m_sampleRate, + m_resolution, + Event().withValue(0.f)); } + +protected: + sv_samplerate_t m_sampleRate; + int m_resolution; + + DeferredNotifier m_notifier; + int m_completion; + + EventSeries m_events; + + mutable QMutex m_mutex; }; #endif - - diff -r ab4fd193262b -r 978c143c767f data/model/IntervalModel.h --- a/data/model/IntervalModel.h Thu May 16 12:54:58 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,194 +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-2008 Chris Cannam and QMUL. - - This program is free software; you can redistribute it and/or - modify it under the terms of the GNU General Public License as - published by the Free Software Foundation; either version 2 of the - License, or (at your option) any later version. See the file - COPYING included with this distribution for more information. -*/ - -#ifndef SV_INTERVAL_MODEL_H -#define SV_INTERVAL_MODEL_H - -#include "SparseValueModel.h" -#include "base/RealTime.h" - -/** - * Model containing sparse data (points with some properties) of which - * the properties include a duration and an arbitrary float value. - * The other properties depend on the point type. - */ - -template -class IntervalModel : public SparseValueModel -{ -public: - IntervalModel(sv_samplerate_t sampleRate, int resolution, - bool notifyOnAdd = true) : - SparseValueModel(sampleRate, resolution, notifyOnAdd) - { } - - IntervalModel(sv_samplerate_t sampleRate, int resolution, - float valueMinimum, float valueMaximum, - bool notifyOnAdd = true) : - SparseValueModel(sampleRate, resolution, - valueMinimum, valueMaximum, - notifyOnAdd) - { } - - /** - * PointTypes have a duration, so this returns all points that span any - * of the given range (as well as the usual additional few before - * and after). Consequently this can be very slow (optimised data - * structures still to be done!). - */ - typename SparseValueModel::PointList getPoints(sv_frame_t start, sv_frame_t end) const override; - - /** - * PointTypes have a duration, so this returns all points that span the - * given frame. Consequently this can be very slow (optimised - * data structures still to be done!). - */ - typename SparseValueModel::PointList getPoints(sv_frame_t frame) const override; - - const typename SparseModel::PointList &getPoints() const override { - return SparseModel::getPoints(); - } - - /** - * TabularModel methods. - */ - - QVariant getData(int row, int column, int role) const override - { - if (column < 2) { - return SparseValueModel::getData - (row, column, role); - } - - typename SparseModel::PointList::const_iterator i - = SparseModel::getPointListIteratorForRow(row); - if (i == SparseModel::m_points.end()) return QVariant(); - - switch (column) { - case 2: - if (role == Qt::EditRole || role == TabularModel::SortRole) return i->value; - else return QString("%1 %2").arg(i->value).arg - (IntervalModel::getScaleUnits()); - case 3: return int(i->duration); //!!! could be better presented - default: return QVariant(); - } - } - - Command *getSetDataCommand(int row, int column, const QVariant &value, int role) override - { - typedef IntervalModel I; - - if (column < 2) { - return SparseValueModel::getSetDataCommand - (row, column, value, role); - } - - if (role != Qt::EditRole) return 0; - typename I::PointList::const_iterator i - = I::getPointListIteratorForRow(row); - if (i == I::m_points.end()) return 0; - typename I::EditCommand *command = new typename I::EditCommand - (this, I::tr("Edit Data")); - - PointType point(*i); - command->deletePoint(point); - - switch (column) { - // column cannot be 0 or 1, those cases were handled above - case 2: point.value = float(value.toDouble()); break; - case 3: point.duration = value.toInt(); break; - } - - command->addPoint(point); - return command->finish(); - } - - bool isColumnTimeValue(int column) const override - { - // NB duration is not a "time value" -- that's for columns - // whose sort ordering is exactly that of the frame time - return (column < 2); - } -}; - -template -typename SparseValueModel::PointList -IntervalModel::getPoints(sv_frame_t start, sv_frame_t end) const -{ - typedef IntervalModel I; - - if (start > end) return typename I::PointList(); - - QMutex &mutex(I::m_mutex); - QMutexLocker locker(&mutex); - - PointType endPoint(end); - - typename I::PointListConstIterator endItr = I::m_points.upper_bound(endPoint); - - if (endItr != I::m_points.end()) ++endItr; - if (endItr != I::m_points.end()) ++endItr; - - typename I::PointList rv; - - for (typename I::PointListConstIterator i = endItr; i != I::m_points.begin(); ) { - --i; - if (i->frame < start) { - if (i->frame + i->duration >= start) { - rv.insert(*i); - } - } else if (i->frame <= end) { - rv.insert(*i); - } - } - - return rv; -} - -template -typename SparseValueModel::PointList -IntervalModel::getPoints(sv_frame_t frame) const -{ - typedef IntervalModel I; - - QMutex &mutex(I::m_mutex); - QMutexLocker locker(&mutex); - - if (I::m_resolution == 0) return typename I::PointList(); - - sv_frame_t start = (frame / I::m_resolution) * I::m_resolution; - sv_frame_t end = start + I::m_resolution; - - PointType endPoint(end); - - typename I::PointListConstIterator endItr = I::m_points.upper_bound(endPoint); - - typename I::PointList rv; - - for (typename I::PointListConstIterator i = endItr; i != I::m_points.begin(); ) { - --i; - if (i->frame < start) { - if (i->frame + i->duration >= start) { - rv.insert(*i); - } - } else if (i->frame <= end) { - rv.insert(*i); - } - } - - return rv; -} - -#endif diff -r ab4fd193262b -r 978c143c767f data/model/Labeller.h --- a/data/model/Labeller.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/Labeller.h Fri May 17 10:02:43 2019 +0100 @@ -16,10 +16,10 @@ #ifndef SV_LABELLER_H #define SV_LABELLER_H -#include "SparseModel.h" -#include "SparseValueModel.h" +#include "base/Selection.h" +#include "base/Event.h" -#include "base/Selection.h" +#include "EventCommands.h" #include @@ -62,6 +62,9 @@ // // 4. re-label a set of points that have already been added to a // model + // + // 5. generate new labelled points in the gaps between other + // points (subdivide), or remove them (winnow) Labeller(ValueType type = ValueNone) : m_type(type), @@ -151,113 +154,160 @@ } } - template - void label(PointType &newPoint, PointType *prevPoint = 0) { + enum Application { + AppliesToThisEvent, + AppliesToPreviousEvent + }; + typedef std::pair Relabelling; + typedef std::pair Revaluing; + + /** + * Return a labelled event based on the given event, previous + * event if supplied, and internal labeller state. The returned + * event replaces either the event provided or the previous event, + * depending on the Application value in the returned pair. + */ + Relabelling + label(Event e, const Event *prev = nullptr) { + + QString label = e.getLabel(); + if (m_type == ValueNone) { - newPoint.label = ""; + label = ""; } else if (m_type == ValueFromTwoLevelCounter) { - newPoint.label = tr("%1.%2").arg(m_counter2).arg(m_counter); + label = tr("%1.%2").arg(m_counter2).arg(m_counter); incrementCounter(); } else if (m_type == ValueFromFrameNumber) { // avoid going through floating-point value - newPoint.label = tr("%1").arg(newPoint.frame); + label = tr("%1").arg(e.getFrame()); } else { - float value = getValueFor(newPoint, prevPoint); - if (actingOnPrevPoint() && prevPoint) { - prevPoint->label = QString("%1").arg(value); - } else { - newPoint.label = QString("%1").arg(value); - } + float value = getValueFor(e, prev); + label = QString("%1").arg(value); + } + + if (actingOnPrevEvent() && prev) { + return { AppliesToPreviousEvent, prev->withLabel(label) }; + } else { + return { AppliesToThisEvent, e.withLabel(label) }; } } + + /** + * Return an event with a value following the labelling scheme, + * based on the given event, previous event if supplied, and + * internal labeller state. The returned event replaces either the + * event provided or the previous event, depending on the + * Application value in the returned pair. + */ + Revaluing + revalue(Event e, const Event *prev = nullptr) { + + float value = e.getValue(); + if (m_type == ValueFromExistingNeighbour) { + if (!prev) { + std::cerr << "ERROR: Labeller::setValue: Previous point required but not provided" << std::endl; + } else { + return { AppliesToThisEvent, e.withValue(prev->getValue()) }; + } + } else { + value = getValueFor(e, prev); + } + + if (actingOnPrevEvent() && prev) { + return { AppliesToPreviousEvent, prev->withValue(value) }; + } else { + return { AppliesToThisEvent, e.withValue(value) }; + } + } + /** - * Relabel all points in the given model that lie within the given - * multi-selection, according to the labelling properties of this - * labeller. Return a command that has been executed but not yet - * added to the history. + * Relabel all events in the given event vector that lie within + * the given multi-selection, according to the labelling + * properties of this labeller. Return a command that has been + * executed but not yet added to the history. */ - template - Command *labelAll(SparseModel &model, MultiSelection *ms) { + Command *labelAll(EventEditable *editable, + MultiSelection *ms, + const EventVector &allEvents) { - auto points(model.getPoints()); - auto command = new typename SparseModel::EditCommand - (&model, tr("Label Points")); + ChangeEventsCommand *command = new ChangeEventsCommand + (editable, tr("Label Points")); - PointType prevPoint(0); - bool havePrevPoint(false); + Event prev; + bool havePrev = false; - for (auto p: points) { + for (auto p: allEvents) { if (ms) { - Selection s(ms->getContainingSelection(p.frame, false)); - if (!s.contains(p.frame)) { - prevPoint = p; - havePrevPoint = true; + Selection s(ms->getContainingSelection(p.getFrame(), false)); + if (!s.contains(p.getFrame())) { + prev = p; + havePrev = true; continue; } } - if (actingOnPrevPoint()) { - if (havePrevPoint) { - command->deletePoint(prevPoint); - label(p, &prevPoint); - command->addPoint(prevPoint); - } + auto labelling = label(p, havePrev ? &prev : nullptr); + + if (labelling.first == AppliesToThisEvent) { + command->remove(p); } else { - command->deletePoint(p); - label(p, &prevPoint); - command->addPoint(p); + command->remove(prev); } - prevPoint = p; - havePrevPoint = true; + command->add(labelling.second); + + prev = p; + havePrev = true; } return command->finish(); } /** - * For each point in the given model (except the last), if that - * point lies within the given multi-selection, add n-1 new points - * at equally spaced intervals between it and the following point. - * Return a command that has been executed but not yet added to - * the history. + * For each event in the given event vector (except the last), if + * that event lies within the given multi-selection, add n-1 new + * events at equally spaced intervals between it and the following + * event. Return a command that has been executed but not yet + * added to the history. */ - template - Command *subdivide(SparseModel &model, MultiSelection *ms, int n) { - - auto points(model.getPoints()); - auto command = new typename SparseModel::EditCommand - (&model, tr("Subdivide Points")); + Command *subdivide(EventEditable *editable, + MultiSelection *ms, + const EventVector &allEvents, + int n) { - for (auto i = points.begin(); i != points.end(); ++i) { + ChangeEventsCommand *command = new ChangeEventsCommand + (editable, tr("Subdivide Points")); + + for (auto i = allEvents.begin(); i != allEvents.end(); ++i) { auto j = i; // require a "next point" even if it's not in selection - if (++j == points.end()) { + if (++j == allEvents.end()) { break; } if (ms) { - Selection s(ms->getContainingSelection(i->frame, false)); - if (!s.contains(i->frame)) { + Selection s(ms->getContainingSelection(i->getFrame(), false)); + if (!s.contains(i->getFrame())) { continue; } } - PointType p(*i); - PointType nextP(*j); + Event p(*i); + Event nextP(*j); // n is the number of subdivisions, so we add n-1 new // points equally spaced between p and nextP for (int m = 1; m < n; ++m) { - sv_frame_t f = p.frame + (m * (nextP.frame - p.frame)) / n; - PointType newPoint(p); - newPoint.frame = f; - newPoint.label = tr("%1.%2").arg(p.label).arg(m+1); - command->addPoint(newPoint); + sv_frame_t f = p.getFrame() + + (m * (nextP.getFrame() - p.getFrame())) / n; + Event newPoint = p + .withFrame(f) + .withLabel(tr("%1.%2").arg(p.getLabel()).arg(m+1)); + command->add(newPoint); } } @@ -265,23 +315,27 @@ } /** + * The opposite of subdivide. Given an event vector, a + * multi-selection, and a number n, remove all but every nth event + * from the vector within the extents of the multi-selection. * Return a command that has been executed but not yet added to * the history. */ - template - Command *winnow(SparseModel &model, MultiSelection *ms, int n) { - - auto points(model.getPoints()); - auto command = new typename SparseModel::EditCommand - (&model, tr("Winnow Points")); + Command *winnow(EventEditable *editable, + MultiSelection *ms, + const EventVector &allEvents, + int n) { + + ChangeEventsCommand *command = new ChangeEventsCommand + (editable, tr("Winnow Points")); int counter = 0; - for (auto p: points) { + for (auto p: allEvents) { if (ms) { - Selection s(ms->getContainingSelection(p.frame, false)); - if (!s.contains(p.frame)) { + Selection s(ms->getContainingSelection(p.getFrame(), false)); + if (!s.contains(p.getFrame())) { counter = 0; continue; } @@ -295,30 +349,12 @@ continue; } - command->deletePoint(p); + command->remove(p); } return command->finish(); } - template - void setValue(PointType &newPoint, PointType *prevPoint = 0) { - if (m_type == ValueFromExistingNeighbour) { - if (!prevPoint) { - std::cerr << "ERROR: Labeller::setValue: Previous point required but not provided" << std::endl; - } else { - newPoint.value = prevPoint->value; - } - } else { - float value = getValueFor(newPoint, prevPoint); - if (actingOnPrevPoint() && prevPoint) { - prevPoint->value = value; - } else { - newPoint.value = value; - } - } - } - bool requiresPrevPoint() const { return (m_type == ValueFromDurationFromPrevious || m_type == ValueFromDurationToNext || @@ -326,15 +362,14 @@ m_type == ValueFromDurationToNext); } - bool actingOnPrevPoint() const { + bool actingOnPrevEvent() const { return (m_type == ValueFromDurationToNext || m_type == ValueFromTempoToNext); } protected: - template - float getValueFor(PointType &newPoint, PointType *prevPoint) - { + float getValueFor(Event p, const Event *prev) { + float value = 0.f; switch (m_type) { @@ -355,14 +390,14 @@ break; case ValueFromFrameNumber: - value = float(newPoint.frame); + value = float(p.getFrame()); break; case ValueFromRealTime: if (m_rate == 0.0) { std::cerr << "ERROR: Labeller::getValueFor: Real-time conversion required, but no sample rate set" << std::endl; } else { - value = float(double(newPoint.frame) / m_rate); + value = float(double(p.getFrame()) / m_rate); } break; @@ -372,10 +407,10 @@ case ValueFromTempoFromPrevious: if (m_rate == 0.0) { std::cerr << "ERROR: Labeller::getValueFor: Real-time conversion required, but no sample rate set" << std::endl; - } else if (!prevPoint) { + } else if (!prev) { std::cerr << "ERROR: Labeller::getValueFor: Time difference required, but only one point provided" << std::endl; } else { - sv_frame_t f0 = prevPoint->frame, f1 = newPoint.frame; + sv_frame_t f0 = prev->getFrame(), f1 = p.getFrame(); if (m_type == ValueFromDurationToNext || m_type == ValueFromDurationFromPrevious) { value = float(double(f1 - f0) / m_rate); @@ -394,9 +429,9 @@ break; case ValueFromLabel: - if (newPoint.label != "") { + if (p.getLabel() != "") { // more forgiving than QString::toFloat() - value = float(atof(newPoint.label.toLocal8Bit())); + value = float(atof(p.getLabel().toLocal8Bit())); } else { value = 0.f; } diff -r ab4fd193262b -r 978c143c767f data/model/Model.cpp --- a/data/model/Model.cpp Thu May 16 12:54:58 2019 +0100 +++ b/data/model/Model.cpp Fri May 17 10:02:43 2019 +0100 @@ -20,11 +20,11 @@ #include -const int Model::COMPLETION_UNKNOWN = -1; +//#define DEBUG_COMPLETION 1 Model::~Model() { -// SVDEBUG << "Model::~Model(" << this << ")" << endl; + SVDEBUG << "Model::~Model(" << this << ")" << endl; if (!m_aboutToDelete) { SVDEBUG << "NOTE: Model(" << this << ", \"" @@ -75,9 +75,10 @@ void Model::aboutToDelete() { -// SVDEBUG << "Model(" << this << ", \"" -// << objectName() << "\", type uri <" -// << m_typeUri << ">)::aboutToDelete()" << endl; + SVDEBUG << "Model(" << this << ", \"" + << objectName() << "\", type name \"" + << getTypeName() << "\", type uri <" + << m_typeUri << ">)::aboutToDelete()" << endl; if (m_aboutToDelete) { SVDEBUG << "WARNING: Model(" << this << ", \"" @@ -100,6 +101,9 @@ void Model::setAlignment(AlignmentModel *alignment) { + SVDEBUG << "Model(" << this << "): accepting alignment model " + << alignment << endl; + if (m_alignment) { m_alignment->aboutToDelete(); delete m_alignment; @@ -161,15 +165,20 @@ int Model::getAlignmentCompletion() const { -// SVDEBUG << "Model::getAlignmentCompletion: m_alignment = " -// << m_alignment << endl; +#ifdef DEBUG_COMPLETION + SVCERR << "Model(" << this << ")::getAlignmentCompletion: m_alignment = " + << m_alignment << endl; +#endif if (!m_alignment) { if (m_sourceModel) return m_sourceModel->getAlignmentCompletion(); else return 100; } int completion = 0; (void)m_alignment->isReady(&completion); -// SVDEBUG << " -> " << completion << endl; +#ifdef DEBUG_COMPLETION + SVCERR << "Model(" << this << ")::getAlignmentCompletion: completion = " << completion + << endl; +#endif return completion; } @@ -200,7 +209,7 @@ { stream << indent; stream << QString("\n") - .arg(getObjectExportId(this)) + .arg(getExportId()) .arg(encodeEntities(objectName())) .arg(getSampleRate()) .arg(getStartFrame()) diff -r ab4fd193262b -r 978c143c767f data/model/Model.h --- a/data/model/Model.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/Model.h Fri May 17 10:02:43 2019 +0100 @@ -55,11 +55,13 @@ virtual sv_frame_t getStartFrame() const = 0; /** - * Return the audio frame at the end of the model, i.e. 1 more - * than the final frame contained within the model. The end frame - * minus the start frame should yield the total duration in frames - * spanned by the model. This is consistent with the definition of - * the end frame of a Selection object. + * Return the audio frame at the end of the model, i.e. the final + * frame contained within the model plus 1 (rounded up to the + * model's "resolution" granularity, if more than 1). The end + * frame minus the start frame should yield the total duration in + * frames (as a multiple of the resolution) spanned by the + * model. This is broadly consistent with the definition of the + * end frame of a Selection object. */ virtual sv_frame_t getEndFrame() const = 0; @@ -131,23 +133,42 @@ /** * Return true if the model has finished loading or calculating * all its data, for a model that is capable of calculating in a - * background thread. The default implementation is appropriate - * for a thread that does not background any work but carries out - * all its calculation from the constructor or accessors. + * background thread. * - * If "completion" is non-NULL, this function should return - * through it an estimated percentage value showing how far - * through the background operation it thinks it is (for progress - * reporting). If it has no way to calculate progress, it may - * return the special value COMPLETION_UNKNOWN. See also - * getCompletion(). + * If "completion" is non-NULL, return through it an estimated + * percentage value showing how far through the background + * operation it thinks it is (for progress reporting). This should + * be identical to the value returned by getCompletion(). + * + * A model that carries out all its calculation from the + * constructor or accessor functions would typically return true + * (and completion == 100) as long as isOK() is true. Other models + * may make the return value here depend on the internal + * completion status. + * + * See also getCompletion(). */ - virtual bool isReady(int *completion = 0) const { - bool ok = isOK(); - if (completion) *completion = (ok ? 100 : 0); - return ok; + virtual bool isReady(int *cp = nullptr) const { + int c = getCompletion(); + if (cp) *cp = c; + if (!isOK()) return false; + else return (c == 100); } - static const int COMPLETION_UNKNOWN; + + /** + * Return an estimated percentage value showing how far through + * any background operation used to calculate or load the model + * data the model thinks it is. Must return 100 when the model is + * complete. + * + * A model that carries out all its calculation from the + * constructor or accessor functions might return 0 if isOK() is + * false and 100 if isOK() is true. Other models may make the + * return value here depend on the internal completion status. + * + * See also isReady(). + */ + virtual int getCompletion() const = 0; /** * If this model imposes a zoom constraint, i.e. some limit to the @@ -236,21 +257,10 @@ QString indent = "", QString extraAttributes = "") const override; - virtual QString toDelimitedDataString(QString delimiter) const { - return toDelimitedDataStringSubset - (delimiter, getStartFrame(), getEndFrame()); - } - virtual QString toDelimitedDataStringWithOptions(QString delimiter, DataExportOptions opts) const { - return toDelimitedDataStringSubsetWithOptions - (delimiter, opts, getStartFrame(), getEndFrame()); - } - virtual QString toDelimitedDataStringSubset(QString, sv_frame_t /* f0 */, sv_frame_t /* f1 */) const { - return ""; - } - virtual QString toDelimitedDataStringSubsetWithOptions(QString delimiter, DataExportOptions, sv_frame_t f0, sv_frame_t f1) const { - // Default implementation supports no options - return toDelimitedDataStringSubset(delimiter, f0, f1); - } + virtual QString toDelimitedDataString(QString delimiter, + DataExportOptions options, + sv_frame_t startFrame, + sv_frame_t duration) const = 0; public slots: void aboutToDelete(); diff -r ab4fd193262b -r 978c143c767f data/model/NoteData.h --- a/data/model/NoteData.h Thu May 16 12:54:58 2019 +0100 +++ /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 - -#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 NoteList; - -class NoteExportable -{ -public: - virtual NoteList getNotes() const = 0; - virtual NoteList getNotesWithin(sv_frame_t startFrame, sv_frame_t endFrame) const = 0; -}; - -#endif diff -r ab4fd193262b -r 978c143c767f data/model/NoteModel.h --- a/data/model/NoteModel.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/NoteModel.h Fri May 17 10:02:43 2019 +0100 @@ -4,7 +4,6 @@ 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 @@ -16,150 +15,251 @@ #ifndef SV_NOTE_MODEL_H #define SV_NOTE_MODEL_H -#include "IntervalModel.h" -#include "NoteData.h" +#include "Model.h" +#include "TabularModel.h" +#include "EventCommands.h" +#include "DeferredNotifier.h" +#include "base/UnitDatabase.h" +#include "base/EventSeries.h" +#include "base/NoteData.h" +#include "base/NoteExportable.h" #include "base/RealTime.h" #include "base/PlayParameterRepository.h" #include "base/Pitch.h" +#include "system/System.h" -/** - * NoteModel -- a concrete IntervalModel for notes. - */ +#include +#include -/** - * Note type for use in a sparse model. All we mean by a "note" is - * something that has an onset time, a single value, a duration, and a - * level. Like other points, it can also have a label. With this - * point type, the model can be thought of as representing a simple - * MIDI-type piano roll, except that the y coordinates (values) do not - * have to be discrete integers. - */ - -struct Note -{ -public: - Note(sv_frame_t _frame) : frame(_frame), value(0.0f), duration(0), level(1.f) { } - Note(sv_frame_t _frame, float _value, sv_frame_t _duration, float _level, QString _label) : - frame(_frame), value(_value), duration(_duration), level(_level), label(_label) { } - - int getDimensions() const { return 3; } - - sv_frame_t frame; - float value; - sv_frame_t duration; - float level; - QString label; - - QString getLabel() const { return label; } - - void toXml(QTextStream &stream, - QString indent = "", - QString extraAttributes = "") const - { - stream << - QString("%1\n") - .arg(indent).arg(frame).arg(value).arg(duration).arg(level) - .arg(XmlExportable::encodeEntities(label)).arg(extraAttributes); - } - - QString toDelimitedDataString(QString delimiter, DataExportOptions opts, sv_samplerate_t sampleRate) const { - QStringList list; - list << RealTime::frame2RealTime(frame, sampleRate).toString().c_str(); - list << QString("%1").arg(value); - list << RealTime::frame2RealTime(duration, sampleRate).toString().c_str(); - if (!(opts & DataExportOmitLevels)) { - list << QString("%1").arg(level); - } - if (label != "") list << label; - return list.join(delimiter); - } - - struct Comparator { - bool operator()(const Note &p1, - const Note &p2) const { - if (p1.frame != p2.frame) return p1.frame < p2.frame; - if (p1.value != p2.value) return p1.value < p2.value; - if (p1.duration != p2.duration) return p1.duration < p2.duration; - if (p1.level != p2.level) return p1.level < p2.level; - return p1.label < p2.label; - } - }; - - struct OrderComparator { - bool operator()(const Note &p1, - const Note &p2) const { - return p1.frame < p2.frame; - } - }; -}; - - -class NoteModel : public IntervalModel, public NoteExportable +class NoteModel : public Model, + public TabularModel, + public NoteExportable, + public EventEditable { Q_OBJECT public: - NoteModel(sv_samplerate_t sampleRate, int resolution, - bool notifyOnAdd = true) : - IntervalModel(sampleRate, resolution, notifyOnAdd), - m_valueQuantization(0) - { + enum Subtype { + NORMAL_NOTE, + FLEXI_NOTE + }; + + NoteModel(sv_samplerate_t sampleRate, + int resolution, + bool notifyOnAdd = true, + Subtype subtype = NORMAL_NOTE) : + m_subtype(subtype), + m_sampleRate(sampleRate), + m_resolution(resolution), + m_valueMinimum(0.f), + m_valueMaximum(0.f), + m_haveExtents(false), + m_valueQuantization(0), + m_units(""), + m_extendTo(0), + m_notifier(this, + notifyOnAdd ? + DeferredNotifier::NOTIFY_ALWAYS : + DeferredNotifier::NOTIFY_DEFERRED), + m_completion(100) { + if (subtype == FLEXI_NOTE) { + m_valueMinimum = 33.f; + m_valueMaximum = 88.f; + } PlayParameterRepository::getInstance()->addPlayable(this); } NoteModel(sv_samplerate_t sampleRate, int resolution, float valueMinimum, float valueMaximum, - bool notifyOnAdd = true) : - IntervalModel(sampleRate, resolution, - valueMinimum, valueMaximum, - notifyOnAdd), - m_valueQuantization(0) - { + bool notifyOnAdd = true, + Subtype subtype = NORMAL_NOTE) : + m_subtype(subtype), + m_sampleRate(sampleRate), + m_resolution(resolution), + m_valueMinimum(valueMinimum), + m_valueMaximum(valueMaximum), + m_haveExtents(true), + m_valueQuantization(0), + m_units(""), + m_extendTo(0), + m_notifier(this, + notifyOnAdd ? + DeferredNotifier::NOTIFY_ALWAYS : + DeferredNotifier::NOTIFY_DEFERRED), + m_completion(100) { PlayParameterRepository::getInstance()->addPlayable(this); } - virtual ~NoteModel() - { + virtual ~NoteModel() { PlayParameterRepository::getInstance()->removePlayable(this); } + QString getTypeName() const override { return tr("Note"); } + Subtype getSubtype() const { return m_subtype; } + bool isSparse() const override { return true; } + bool isOK() const override { return true; } + + sv_frame_t getStartFrame() const override { + return m_events.getStartFrame(); + } + sv_frame_t getEndFrame() const override { + if (m_events.isEmpty()) return 0; + sv_frame_t e = m_events.getEndFrame(); + if (e % m_resolution == 0) return e; + else return (e / m_resolution + 1) * m_resolution; + } + + sv_samplerate_t getSampleRate() const override { return m_sampleRate; } + int getResolution() const { return m_resolution; } + + bool canPlay() const override { return true; } + QString getDefaultPlayClipId() const override { + return "elecpiano"; + } + + QString getScaleUnits() const { return m_units; } + void setScaleUnits(QString units) { + m_units = units; + UnitDatabase::getInstance()->registerUnit(units); + } + float getValueQuantization() const { return m_valueQuantization; } void setValueQuantization(float q) { m_valueQuantization = q; } - QString getTypeName() const override { return tr("Note"); } + float getValueMinimum() const { return m_valueMinimum; } + float getValueMaximum() const { return m_valueMaximum; } + + int getCompletion() const override { return m_completion; } - bool canPlay() const override { return true; } + void setCompletion(int completion, bool update = true) { - QString getDefaultPlayClipId() const override - { - return "elecpiano"; + { QMutexLocker locker(&m_mutex); + if (m_completion == completion) return; + m_completion = completion; + } + + if (update) { + m_notifier.makeDeferredNotifications(); + } + + emit completionChanged(); + + if (completion == 100) { + // henceforth: + m_notifier.switchMode(DeferredNotifier::NOTIFY_ALWAYS); + emit modelChanged(); + } } - void toXml(QTextStream &out, - QString indent = "", - QString extraAttributes = "") const override - { - std::cerr << "NoteModel::toXml: extraAttributes = \"" - << extraAttributes.toStdString() << std::endl; + /** + * Query methods. + */ - IntervalModel::toXml - (out, - indent, - QString("%1 subtype=\"note\" valueQuantization=\"%2\"") - .arg(extraAttributes).arg(m_valueQuantization)); + int getEventCount() const { + return m_events.count(); + } + bool isEmpty() const { + return m_events.isEmpty(); + } + bool containsEvent(const Event &e) const { + return m_events.contains(e); + } + EventVector getAllEvents() const { + return m_events.getAllEvents(); + } + EventVector getEventsSpanning(sv_frame_t f, sv_frame_t duration) const { + return m_events.getEventsSpanning(f, duration); + } + EventVector getEventsCovering(sv_frame_t f) const { + return m_events.getEventsCovering(f); + } + EventVector getEventsWithin(sv_frame_t f, sv_frame_t duration) const { + return m_events.getEventsWithin(f, duration); + } + EventVector getEventsStartingWithin(sv_frame_t f, sv_frame_t duration) const { + return m_events.getEventsStartingWithin(f, duration); + } + EventVector getEventsStartingAt(sv_frame_t f) const { + return m_events.getEventsStartingAt(f); + } + bool getNearestEventMatching(sv_frame_t startSearchAt, + std::function predicate, + EventSeries::Direction direction, + Event &found) const { + return m_events.getNearestEventMatching + (startSearchAt, predicate, direction, found); + } + + /** + * Editing methods. + */ + void add(Event e) override { + + bool allChange = false; + + { + QMutexLocker locker(&m_mutex); + m_events.add(e); + + float v = e.getValue(); + if (!ISNAN(v) && !ISINF(v)) { + if (!m_haveExtents || v < m_valueMinimum) { + m_valueMinimum = v; allChange = true; + } + if (!m_haveExtents || v > m_valueMaximum) { + m_valueMaximum = v; allChange = true; + } + m_haveExtents = true; + } + } + + m_notifier.update(e.getFrame(), e.getDuration() + m_resolution); + + if (allChange) { + emit modelChanged(); + } + } + + void remove(Event e) override { + { + QMutexLocker locker(&m_mutex); + m_events.remove(e); + } + emit modelChangedWithin(e.getFrame(), + e.getFrame() + e.getDuration() + m_resolution); } /** * TabularModel methods. */ + + int getRowCount() const override { + return m_events.count(); + } - int getColumnCount() const override - { + int getColumnCount() const override { return 6; } - QString getHeading(int column) const override - { + bool isColumnTimeValue(int column) const override { + // NB duration is not a "time value" -- that's for columns + // whose sort ordering is exactly that of the frame time + return (column < 2); + } + + sv_frame_t getFrameForRow(int row) const override { + if (row < 0 || row >= m_events.count()) { + return 0; + } + Event e = m_events.getEventByIndex(row); + return e.getFrame(); + } + + int getRowForFrame(sv_frame_t frame) const override { + return m_events.getIndexForEvent(Event(frame)); + } + + QString getHeading(int column) const override { switch (column) { case 0: return tr("Time"); case 1: return tr("Frame"); @@ -171,43 +271,47 @@ } } - QVariant getData(int row, int column, int role) const override - { - if (column < 4) { - return IntervalModel::getData(row, column, role); + QVariant getData(int row, int column, int role) const override { + + if (row < 0 || row >= m_events.count()) { + return QVariant(); } - PointListConstIterator i = getPointListIteratorForRow(row); - if (i == m_points.end()) return QVariant(); + Event e = m_events.getEventByIndex(row); switch (column) { - case 4: return i->level; - case 5: return i->label; + case 0: return adaptFrameForRole(e.getFrame(), getSampleRate(), role); + case 1: return int(e.getFrame()); + case 2: return adaptValueForRole(e.getValue(), getScaleUnits(), role); + case 3: return int(e.getDuration()); + case 4: return e.getLevel(); + case 5: return e.getLabel(); default: return QVariant(); } } - Command *getSetDataCommand(int row, int column, const QVariant &value, int role) override - { - if (column < 4) { - return IntervalModel::getSetDataCommand - (row, column, value, role); + Command *getSetDataCommand(int row, int column, const QVariant &value, int role) override { + + if (row < 0 || row >= m_events.count()) return nullptr; + if (role != Qt::EditRole) return nullptr; + + Event e0 = m_events.getEventByIndex(row); + Event e1; + + switch (column) { + case 0: e1 = e0.withFrame(sv_frame_t(round(value.toDouble() * + getSampleRate()))); break; + case 1: e1 = e0.withFrame(value.toInt()); break; + case 2: e1 = e0.withValue(float(value.toDouble())); break; + case 3: e1 = e0.withDuration(value.toInt()); break; + case 4: e1 = e0.withLevel(float(value.toDouble())); break; + case 5: e1 = e0.withLabel(value.toString()); break; } - if (role != Qt::EditRole) return 0; - PointListConstIterator i = getPointListIteratorForRow(row); - if (i == m_points.end()) return 0; - EditCommand *command = new EditCommand(this, tr("Edit Data")); - - Point point(*i); - command->deletePoint(point); - - switch (column) { - case 4: point.level = float(value.toDouble()); break; - case 5: point.label = value.toString(); break; - } - - command->addPoint(point); + ChangeEventsCommand *command = + new ChangeEventsCommand(this, tr("Edit Data")); + command->remove(e0); + command->add(e1); return command->finish(); } @@ -222,45 +326,98 @@ */ NoteList getNotes() const override { - return getNotesWithin(getStartFrame(), getEndFrame()); + return getNotesStartingWithin(getStartFrame(), + getEndFrame() - getStartFrame()); } - NoteList getNotesWithin(sv_frame_t startFrame, sv_frame_t endFrame) const override { - - PointList points = getPoints(startFrame, endFrame); + NoteList getNotesActiveAt(sv_frame_t frame) const override { + NoteList notes; + EventVector ee = m_events.getEventsCovering(frame); + for (const auto &e: ee) { + notes.push_back(e.toNoteData(getSampleRate(), + getScaleUnits() != "Hz")); + } + return notes; + } + + NoteList getNotesStartingWithin(sv_frame_t startFrame, + sv_frame_t duration) const override { - for (PointList::iterator pli = - points.begin(); pli != points.end(); ++pli) { - - sv_frame_t duration = pli->duration; - if (duration == 0 || duration == 1) { - duration = sv_frame_t(getSampleRate() / 20); - } - - int pitch = int(lrintf(pli->value)); - - int velocity = 100; - if (pli->level > 0.f && pli->level <= 1.f) { - velocity = int(lrintf(pli->level * 127)); - } - - NoteData note(pli->frame, duration, pitch, velocity); - - if (getScaleUnits() == "Hz") { - note.frequency = pli->value; - note.midiPitch = Pitch::getPitchForFrequency(note.frequency); - note.isMidiPitchQuantized = false; - } - - notes.push_back(note); + NoteList notes; + EventVector ee = m_events.getEventsStartingWithin(startFrame, duration); + for (const auto &e: ee) { + notes.push_back(e.toNoteData(getSampleRate(), + getScaleUnits() != "Hz")); } - return notes; } + /** + * XmlExportable methods. + */ + + void toXml(QTextStream &out, + QString indent = "", + QString extraAttributes = "") const override { + + //!!! what is valueQuantization used for? + + Model::toXml + (out, + indent, + QString("type=\"sparse\" dimensions=\"3\" resolution=\"%1\" " + "notifyOnAdd=\"%2\" dataset=\"%3\" subtype=\"%4\" " + "valueQuantization=\"%5\" minimum=\"%6\" maximum=\"%7\" " + "units=\"%8\" %9") + .arg(m_resolution) + .arg("true") // always true after model reaches 100% - + // subsequent events are always notified + .arg(m_events.getExportId()) + .arg(m_subtype == FLEXI_NOTE ? "flexinote" : "note") + .arg(m_valueQuantization) + .arg(m_valueMinimum) + .arg(m_valueMaximum) + .arg(encodeEntities(m_units)) + .arg(extraAttributes)); + + m_events.toXml(out, indent, QString("dimensions=\"3\"")); + } + + QString toDelimitedDataString(QString delimiter, + DataExportOptions options, + sv_frame_t startFrame, + sv_frame_t duration) const override { + return m_events.toDelimitedDataString + (delimiter, + options, + startFrame, + duration, + m_sampleRate, + m_resolution, + Event().withValue(0.f).withDuration(0.f).withLevel(0.f)); + } + protected: + Subtype m_subtype; + sv_samplerate_t m_sampleRate; + int m_resolution; + + float m_valueMinimum; + float m_valueMaximum; + bool m_haveExtents; float m_valueQuantization; + QString m_units; + sv_frame_t m_extendTo; + DeferredNotifier m_notifier; + int m_completion; + + EventSeries m_events; + + mutable QMutex m_mutex; + + //!!! do we have general docs for ownership and synchronisation of models? + // this might be a good opportunity to stop using bare pointers to them }; #endif diff -r ab4fd193262b -r 978c143c767f data/model/PathModel.h --- a/data/model/PathModel.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/PathModel.h Fri May 17 10:02:43 2019 +0100 @@ -17,26 +17,26 @@ #define SV_PATH_MODEL_H #include "Model.h" -#include "SparseModel.h" +#include "DeferredNotifier.h" #include "base/RealTime.h" #include "base/BaseTypes.h" +#include "base/XmlExportable.h" +#include "base/RealTime.h" + #include - +#include struct PathPoint { - PathPoint(sv_frame_t _frame) : frame(_frame), mapframe(_frame) { } + PathPoint(sv_frame_t _frame) : + frame(_frame), mapframe(_frame) { } PathPoint(sv_frame_t _frame, sv_frame_t _mapframe) : frame(_frame), mapframe(_mapframe) { } - int getDimensions() const { return 2; } - sv_frame_t frame; sv_frame_t mapframe; - QString getLabel() const { return ""; } - void toXml(QTextStream &stream, QString indent = "", QString extraAttributes = "") const { stream << QString("%1\n") @@ -51,44 +51,178 @@ return list.join(delimiter); } - struct Comparator { - bool operator()(const PathPoint &p1, const PathPoint &p2) const { - if (p1.frame != p2.frame) return p1.frame < p2.frame; - return p1.mapframe < p2.mapframe; - } - }; - - struct OrderComparator { - bool operator()(const PathPoint &p1, const PathPoint &p2) const { - return p1.frame < p2.frame; - } - }; + bool operator<(const PathPoint &p2) const { + if (frame != p2.frame) return frame < p2.frame; + return mapframe < p2.mapframe; + } }; -class PathModel : public SparseModel +class PathModel : public Model { public: - PathModel(sv_samplerate_t sampleRate, int resolution, bool notify = true) : - SparseModel(sampleRate, resolution, notify) { } + typedef std::set PointList; - void toXml(QTextStream &out, - QString indent = "", - QString extraAttributes = "") const override - { - SparseModel::toXml - (out, - indent, - QString("%1 subtype=\"path\"") - .arg(extraAttributes)); + PathModel(sv_samplerate_t sampleRate, + int resolution, + bool notifyOnAdd = true) : + m_sampleRate(sampleRate), + m_resolution(resolution), + m_notifier(this, + notifyOnAdd ? + DeferredNotifier::NOTIFY_ALWAYS : + DeferredNotifier::NOTIFY_DEFERRED), + m_completion(100), + m_start(0), + m_end(0) { + } + + QString getTypeName() const override { return tr("Path"); } + bool isSparse() const override { return true; } + bool isOK() const override { return true; } + + sv_frame_t getStartFrame() const override { + return m_start; + } + sv_frame_t getEndFrame() const override { + return m_end; + } + + sv_samplerate_t getSampleRate() const override { return m_sampleRate; } + int getResolution() const { return m_resolution; } + + int getCompletion() const override { return m_completion; } + + void setCompletion(int completion, bool update = true) { + + { QMutexLocker locker(&m_mutex); + if (m_completion == completion) return; + m_completion = completion; + } + + if (update) { + m_notifier.makeDeferredNotifications(); + } + + emit completionChanged(); + + if (completion == 100) { + // henceforth: + m_notifier.switchMode(DeferredNotifier::NOTIFY_ALWAYS); + emit modelChanged(); + } } /** - * TabularModel is inherited via SparseModel, but we don't need it here. + * Query methods. */ - QString getHeading(int) const override { return ""; } - bool isColumnTimeValue(int) const override { return false; } - SortType getSortType(int) const override { return SortNumeric; } + int getPointCount() const { + return int(m_points.size()); + } + const PointList &getPoints() const { + return m_points; + } + /** + * Editing methods. + */ + void add(PathPoint p) { + + { QMutexLocker locker(&m_mutex); + m_points.insert(p); + + if (m_start == m_end) { + m_start = p.frame; + m_end = m_start + m_resolution; + } else { + if (p.frame < m_start) { + m_start = p.frame; + } + if (p.frame + m_resolution > m_end) { + m_end = p.frame + m_resolution; + } + } + } + + m_notifier.update(p.frame, m_resolution); + } + + void remove(PathPoint p) { + { QMutexLocker locker(&m_mutex); + m_points.erase(p); + } + + emit modelChangedWithin(p.frame, p.frame + m_resolution); + } + + void clear() { + { QMutexLocker locker(&m_mutex); + m_start = m_end = 0; + m_points.clear(); + } + } + + /** + * XmlExportable methods. + */ + void toXml(QTextStream &out, + QString indent = "", + QString extraAttributes = "") const override { + + // Our dataset doesn't have its own export ID, we just use + // ours. Actually any model could do that, since datasets + // aren't in the same id-space as models when re-read + + Model::toXml + (out, + indent, + QString("type=\"sparse\" dimensions=\"2\" resolution=\"%1\" " + "notifyOnAdd=\"%2\" dataset=\"%3\" subtype=\"path\" %4") + .arg(m_resolution) + .arg("true") // always true after model reaches 100% - + // subsequent points are always notified + .arg(getExportId()) + .arg(extraAttributes)); + + out << indent << QString("\n") + .arg(getExportId()); + + for (PathPoint p: m_points) { + p.toXml(out, indent + " ", ""); + } + + out << indent << "\n"; + } + + QString toDelimitedDataString(QString delimiter, + DataExportOptions, + sv_frame_t startFrame, + sv_frame_t duration) const override { + + QString s; + for (PathPoint p: m_points) { + if (p.frame < startFrame) continue; + if (p.frame >= startFrame + duration) break; + s += QString("%1%2%3\n") + .arg(p.frame) + .arg(delimiter) + .arg(p.mapframe); + } + + return s; + } + +protected: + sv_samplerate_t m_sampleRate; + int m_resolution; + + DeferredNotifier m_notifier; + int m_completion; + + sv_frame_t m_start; + sv_frame_t m_end; + PointList m_points; + + mutable QMutex m_mutex; }; diff -r ab4fd193262b -r 978c143c767f data/model/ReadOnlyWaveFileModel.h --- a/data/model/ReadOnlyWaveFileModel.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/ReadOnlyWaveFileModel.h Fri May 17 10:02:43 2019 +0100 @@ -54,6 +54,11 @@ bool isOK() const override; bool isReady(int *) const override; + int getCompletion() const override { + int c = 0; + (void)isReady(&c); + return c; + } const ZoomConstraint *getZoomConstraint() const override { return &m_zoomConstraint; } diff -r ab4fd193262b -r 978c143c767f data/model/RegionModel.h --- a/data/model/RegionModel.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/RegionModel.h Fri May 17 10:02:43 2019 +0100 @@ -4,7 +4,6 @@ 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 @@ -16,105 +15,88 @@ #ifndef SV_REGION_MODEL_H #define SV_REGION_MODEL_H -#include "IntervalModel.h" +#include "EventCommands.h" +#include "TabularModel.h" +#include "Model.h" +#include "DeferredNotifier.h" + #include "base/RealTime.h" +#include "base/EventSeries.h" +#include "base/UnitDatabase.h" + +#include "system/System.h" + +#include /** - * RegionModel -- a concrete IntervalModel for intervals associated - * with a value, which we call regions for no very compelling reason. + * RegionModel -- a model for intervals associated with a value, which + * we call regions for no very compelling reason. */ - -/** - * Region "point" type. A region is something that has an onset time, - * a single value, and a duration. Like other points, it can also - * have a label. - * - * This is called RegionRec instead of Region to avoid name collisions - * with the X11 Region struct. Bah. - */ - -struct RegionRec -{ -public: - RegionRec() : frame(0), value(0.f), duration(0) { } - RegionRec(sv_frame_t _frame) : frame(_frame), value(0.0f), duration(0) { } - RegionRec(sv_frame_t _frame, float _value, sv_frame_t _duration, QString _label) : - frame(_frame), value(_value), duration(_duration), label(_label) { } - - int getDimensions() const { return 3; } - - sv_frame_t frame; - float value; - sv_frame_t duration; - QString label; - - QString getLabel() const { return label; } - - void toXml(QTextStream &stream, - QString indent = "", - QString extraAttributes = "") const - { - stream << - QString("%1\n") - .arg(indent).arg(frame).arg(value).arg(duration) - .arg(XmlExportable::encodeEntities(label)).arg(extraAttributes); - } - - QString toDelimitedDataString(QString delimiter, DataExportOptions, sv_samplerate_t sampleRate) const - { - QStringList list; - list << RealTime::frame2RealTime(frame, sampleRate).toString().c_str(); - list << QString("%1").arg(value); - list << RealTime::frame2RealTime(duration, sampleRate).toString().c_str(); - if (label != "") list << label; - return list.join(delimiter); - } - - struct Comparator { - bool operator()(const RegionRec &p1, - const RegionRec &p2) const { - if (p1.frame != p2.frame) return p1.frame < p2.frame; - if (p1.value != p2.value) return p1.value < p2.value; - if (p1.duration != p2.duration) return p1.duration < p2.duration; - return p1.label < p2.label; - } - }; - - struct OrderComparator { - bool operator()(const RegionRec &p1, - const RegionRec &p2) const { - return p1.frame < p2.frame; - } - }; -}; - - -class RegionModel : public IntervalModel +class RegionModel : public Model, + public TabularModel, + public EventEditable { Q_OBJECT public: - RegionModel(sv_samplerate_t sampleRate, int resolution, + RegionModel(sv_samplerate_t sampleRate, + int resolution, bool notifyOnAdd = true) : - IntervalModel(sampleRate, resolution, notifyOnAdd), + m_sampleRate(sampleRate), + m_resolution(resolution), + m_valueMinimum(0.f), + m_valueMaximum(0.f), + m_haveExtents(false), m_valueQuantization(0), - m_haveDistinctValues(false) - { + m_haveDistinctValues(false), + m_notifier(this, + notifyOnAdd ? + DeferredNotifier::NOTIFY_ALWAYS : + DeferredNotifier::NOTIFY_DEFERRED), + m_completion(100) { } RegionModel(sv_samplerate_t sampleRate, int resolution, float valueMinimum, float valueMaximum, bool notifyOnAdd = true) : - IntervalModel(sampleRate, resolution, - valueMinimum, valueMaximum, - notifyOnAdd), + m_sampleRate(sampleRate), + m_resolution(resolution), + m_valueMinimum(valueMinimum), + m_valueMaximum(valueMaximum), + m_haveExtents(true), m_valueQuantization(0), - m_haveDistinctValues(false) - { + m_haveDistinctValues(false), + m_notifier(this, + notifyOnAdd ? + DeferredNotifier::NOTIFY_ALWAYS : + DeferredNotifier::NOTIFY_DEFERRED), + m_completion(100) { } - virtual ~RegionModel() - { + virtual ~RegionModel() { + } + + QString getTypeName() const override { return tr("Region"); } + bool isSparse() const override { return true; } + bool isOK() const override { return true; } + + sv_frame_t getStartFrame() const override { + return m_events.getStartFrame(); + } + sv_frame_t getEndFrame() const override { + if (m_events.isEmpty()) return 0; + sv_frame_t e = m_events.getEndFrame(); + if (e % m_resolution == 0) return e; + else return (e / m_resolution + 1) * m_resolution; + } + + sv_samplerate_t getSampleRate() const override { return m_sampleRate; } + int getResolution() const { return m_resolution; } + + QString getScaleUnits() const { return m_units; } + void setScaleUnits(QString units) { + m_units = units; + UnitDatabase::getInstance()->registerUnit(units); } float getValueQuantization() const { return m_valueQuantization; } @@ -122,33 +104,143 @@ bool haveDistinctValues() const { return m_haveDistinctValues; } - QString getTypeName() const override { return tr("Region"); } + float getValueMinimum() const { return m_valueMinimum; } + float getValueMaximum() const { return m_valueMaximum; } + + int getCompletion() const override { return m_completion; } - void toXml(QTextStream &out, - QString indent = "", - QString extraAttributes = "") const override - { - std::cerr << "RegionModel::toXml: extraAttributes = \"" - << extraAttributes.toStdString() << std::endl; + void setCompletion(int completion, bool update = true) { - IntervalModel::toXml - (out, - indent, - QString("%1 subtype=\"region\" valueQuantization=\"%2\"") - .arg(extraAttributes).arg(m_valueQuantization)); + { QMutexLocker locker(&m_mutex); + if (m_completion == completion) return; + m_completion = completion; + } + + if (update) { + m_notifier.makeDeferredNotifications(); + } + + emit completionChanged(); + + if (completion == 100) { + // henceforth: + m_notifier.switchMode(DeferredNotifier::NOTIFY_ALWAYS); + emit modelChanged(); + } } /** + * Query methods. + */ + int getEventCount() const { + return m_events.count(); + } + bool isEmpty() const { + return m_events.isEmpty(); + } + bool containsEvent(const Event &e) const { + return m_events.contains(e); + } + EventVector getAllEvents() const { + return m_events.getAllEvents(); + } + EventVector getEventsSpanning(sv_frame_t f, sv_frame_t duration) const { + return m_events.getEventsSpanning(f, duration); + } + EventVector getEventsCovering(sv_frame_t f) const { + return m_events.getEventsCovering(f); + } + EventVector getEventsWithin(sv_frame_t f, sv_frame_t duration) const { + return m_events.getEventsWithin(f, duration); + } + EventVector getEventsStartingWithin(sv_frame_t f, sv_frame_t duration) const { + return m_events.getEventsStartingWithin(f, duration); + } + EventVector getEventsStartingAt(sv_frame_t f) const { + return m_events.getEventsStartingAt(f); + } + bool getNearestEventMatching(sv_frame_t startSearchAt, + std::function predicate, + EventSeries::Direction direction, + Event &found) const { + return m_events.getNearestEventMatching + (startSearchAt, predicate, direction, found); + } + + /** + * Editing methods. + */ + void add(Event e) override { + + bool allChange = false; + + { + QMutexLocker locker(&m_mutex); + m_events.add(e); + + float v = e.getValue(); + if (!ISNAN(v) && !ISINF(v)) { + if (!m_haveExtents || v < m_valueMinimum) { + m_valueMinimum = v; allChange = true; + } + if (!m_haveExtents || v > m_valueMaximum) { + m_valueMaximum = v; allChange = true; + } + m_haveExtents = true; + } + + if (e.hasValue() && e.getValue() != 0.f) { + m_haveDistinctValues = true; + } + } + + m_notifier.update(e.getFrame(), e.getDuration() + m_resolution); + + if (allChange) { + emit modelChanged(); + } + } + + void remove(Event e) override { + { + QMutexLocker locker(&m_mutex); + m_events.remove(e); + } + emit modelChangedWithin(e.getFrame(), + e.getFrame() + e.getDuration() + m_resolution); + } + + /** * TabularModel methods. */ - - int getColumnCount() const override - { + + int getRowCount() const override { + return m_events.count(); + } + + int getColumnCount() const override { return 5; } - QString getHeading(int column) const override - { + bool isColumnTimeValue(int column) const override { + // NB duration is not a "time value" -- that's for columns + // whose sort ordering is exactly that of the frame time + return (column < 2); + } + + sv_frame_t getFrameForRow(int row) const override { + if (row < 0 || row >= m_events.count()) { + return 0; + } + Event e = m_events.getEventByIndex(row); + return e.getFrame(); + } + + int getRowForFrame(sv_frame_t frame) const override { + return m_events.getIndexForEvent(Event(frame)); + } + + QString getHeading(int column) const override { switch (column) { case 0: return tr("Time"); case 1: return tr("Frame"); @@ -159,59 +251,112 @@ } } - QVariant getData(int row, int column, int role) const override - { - if (column < 4) { - return IntervalModel::getData(row, column, role); + SortType getSortType(int column) const override { + if (column == 4) return SortAlphabetical; + return SortNumeric; + } + + QVariant getData(int row, int column, int role) const override { + + if (row < 0 || row >= m_events.count()) { + return QVariant(); } - PointListConstIterator i = getPointListIteratorForRow(row); - if (i == m_points.end()) return QVariant(); + Event e = m_events.getEventByIndex(row); switch (column) { - case 4: return i->label; + case 0: return adaptFrameForRole(e.getFrame(), getSampleRate(), role); + case 1: return int(e.getFrame()); + case 2: return adaptValueForRole(e.getValue(), getScaleUnits(), role); + case 3: return int(e.getDuration()); + case 4: return e.getLabel(); default: return QVariant(); } } - Command *getSetDataCommand(int row, int column, const QVariant &value, int role) override - { - if (column < 4) { - return IntervalModel::getSetDataCommand - (row, column, value, role); + Command *getSetDataCommand(int row, int column, const QVariant &value, int role) override { + + if (row < 0 || row >= m_events.count()) return nullptr; + if (role != Qt::EditRole) return nullptr; + + Event e0 = m_events.getEventByIndex(row); + Event e1; + + switch (column) { + case 0: e1 = e0.withFrame(sv_frame_t(round(value.toDouble() * + getSampleRate()))); break; + case 1: e1 = e0.withFrame(value.toInt()); break; + case 2: e1 = e0.withValue(float(value.toDouble())); break; + case 3: e1 = e0.withDuration(value.toInt()); break; + case 4: e1 = e0.withLabel(value.toString()); break; } - if (role != Qt::EditRole) return 0; - PointListIterator i = getPointListIteratorForRow(row); - if (i == m_points.end()) return 0; - EditCommand *command = new EditCommand(this, tr("Edit Data")); - - Point point(*i); - command->deletePoint(point); - - switch (column) { - case 4: point.label = value.toString(); break; - } - - command->addPoint(point); + ChangeEventsCommand *command = + new ChangeEventsCommand(this, tr("Edit Data")); + command->remove(e0); + command->add(e1); return command->finish(); } - SortType getSortType(int column) const override - { - if (column == 4) return SortAlphabetical; - return SortNumeric; + + /** + * XmlExportable methods. + */ + void toXml(QTextStream &out, + QString indent = "", + QString extraAttributes = "") const override { + + Model::toXml + (out, + indent, + QString("type=\"sparse\" dimensions=\"3\" resolution=\"%1\" " + "notifyOnAdd=\"%2\" dataset=\"%3\" subtype=\"%4\" " + "valueQuantization=\"%5\" minimum=\"%6\" maximum=\"%7\" " + "units=\"%8\" %9") + .arg(m_resolution) + .arg("true") // always true after model reaches 100% - + // subsequent events are always notified + .arg(m_events.getExportId()) + .arg("region") + .arg(m_valueQuantization) + .arg(m_valueMinimum) + .arg(m_valueMaximum) + .arg(encodeEntities(m_units)) + .arg(extraAttributes)); + + m_events.toXml(out, indent, QString("dimensions=\"3\"")); } - void addPoint(const Point &point) override - { - if (point.value != 0.f) m_haveDistinctValues = true; - IntervalModel::addPoint(point); + QString toDelimitedDataString(QString delimiter, + DataExportOptions options, + sv_frame_t startFrame, + sv_frame_t duration) const override { + return m_events.toDelimitedDataString + (delimiter, + options, + startFrame, + duration, + m_sampleRate, + m_resolution, + Event().withValue(0.f).withDuration(m_resolution)); } - + protected: + sv_samplerate_t m_sampleRate; + int m_resolution; + + float m_valueMinimum; + float m_valueMaximum; + bool m_haveExtents; float m_valueQuantization; bool m_haveDistinctValues; + QString m_units; + DeferredNotifier m_notifier; + int m_completion; + + EventSeries m_events; + + mutable QMutex m_mutex; }; #endif diff -r ab4fd193262b -r 978c143c767f data/model/SparseModel.h --- a/data/model/SparseModel.h Thu May 16 12:54:58 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1003 +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_SPARSE_MODEL_H -#define SV_SPARSE_MODEL_H - -#include "Model.h" -#include "TabularModel.h" -#include "base/Command.h" -#include "base/RealTime.h" -#include "system/System.h" - -#include - -#include -#include -#include -#include - -#include - -#include -#include - -/** - * Model containing sparse data (points with some properties). The - * properties depend on the point type. - */ - -template -class SparseModel : public Model, - public TabularModel -{ - // If we omit the Q_OBJECT macro, lupdate complains. - - // If we include it, moc fails (can't handle template classes). - - // If we omit it, lupdate still seems to emit translatable - // messages for the tr() strings in here. So I guess we omit it. - -public: - SparseModel(sv_samplerate_t sampleRate, int resolution, - bool notifyOnAdd = true); - virtual ~SparseModel() { } - - bool isOK() const override { return true; } - sv_frame_t getStartFrame() const override; - sv_frame_t getEndFrame() const override; - sv_samplerate_t getSampleRate() const override { return m_sampleRate; } - - // Number of frames of the underlying sample rate that this model - // is capable of resolving to. For example, if m_resolution == 10 - // then every point in this model will be at a multiple of 10 - // sample frames and should be considered to cover a window ending - // 10 sample frames later. - virtual int getResolution() const { - return m_resolution ? m_resolution : 1; - } - virtual void setResolution(int resolution); - - // Extend the end of the model. If this is set to something beyond - // the end of the final point in the model, then getEndFrame() - // will return this value. Otherwise getEndFrame() will return the - // end of the final point. (This is used by the Tony application) - virtual void extendEndFrame(sv_frame_t to) { m_extendTo = to; } - - typedef PointType Point; - typedef std::multiset PointList; - typedef typename PointList::iterator PointListIterator; - typedef typename PointList::const_iterator PointListConstIterator; - - /** - * Return whether the model is empty or not. - */ - virtual bool isEmpty() const; - - /** - * Get the total number of points in the model. - */ - virtual int getPointCount() const; - - /** - * Get all points. - */ - virtual const PointList &getPoints() const; - - /** - * Get all of the points in this model between the given - * boundaries (in frames), as well as up to two points before and - * after the boundaries. If you need exact boundaries, check the - * point coordinates in the returned list. - */ - virtual PointList getPoints(sv_frame_t start, sv_frame_t end) const; - - /** - * Get all points that cover the given frame number, taking the - * resolution of the model into account. - */ - virtual PointList getPoints(sv_frame_t frame) const; - - /** - * Return all points that share the nearest frame number prior to - * the given one at which there are any points. - */ - virtual PointList getPreviousPoints(sv_frame_t frame) const; - - /** - * Return all points that share the nearest frame number - * subsequent to the given one at which there are any points. - */ - virtual PointList getNextPoints(sv_frame_t frame) const; - - /** - * Remove all points. - */ - virtual void clear(); - - /** - * Add a point. - */ - virtual void addPoint(const PointType &point); - - /** - * Remove a point. Points are not necessarily unique, so this - * function will remove the first point that compares equal to the - * supplied one using Point::Comparator. Other identical points - * may remain in the model. - */ - virtual void deletePoint(const PointType &point); - - /** - * Return true if the given point is found in this model, false - * otherwise. - */ - virtual bool containsPoint(const PointType &point); - - bool isReady(int *completion = 0) const override { - bool ready = isOK() && (m_completion == 100); - if (completion) *completion = m_completion; - return ready; - } - - virtual void setCompletion(int completion, bool update = true); - virtual int getCompletion() const { return m_completion; } - - virtual bool hasTextLabels() const { return m_hasTextLabels; } - - bool isSparse() const override { return true; } - - QString getTypeName() const override { return tr("Sparse"); } - - virtual QString getXmlOutputType() const { return "sparse"; } - - void toXml(QTextStream &out, - QString indent = "", - QString extraAttributes = "") const override; - - QString toDelimitedDataString(QString delimiter) const override { - return toDelimitedDataStringWithOptions - (delimiter, DataExportDefaults); - } - - QString toDelimitedDataStringWithOptions(QString delimiter, - DataExportOptions opts) const override { - return toDelimitedDataStringSubsetWithOptions - (delimiter, opts, - std::min(getStartFrame(), sv_frame_t(0)), getEndFrame()); - } - - QString toDelimitedDataStringSubset(QString delimiter, sv_frame_t f0, sv_frame_t f1) const override { - return toDelimitedDataStringSubsetWithOptions - (delimiter, DataExportDefaults, f0, f1); - } - - QString toDelimitedDataStringSubsetWithOptions(QString delimiter, DataExportOptions opts, sv_frame_t f0, sv_frame_t f1) const override { - if (opts & DataExportFillGaps) { - return toDelimitedDataStringSubsetFilled(delimiter, opts, f0, f1); - } else { - QString s; - for (PointListConstIterator i = m_points.begin(); i != m_points.end(); ++i) { - if (i->frame >= f0 && i->frame < f1) { - s += i->toDelimitedDataString(delimiter, opts, m_sampleRate) + "\n"; - } - } - return s; - } - } - - /** - * Command to add a point, with undo. - */ - class AddPointCommand : public Command - { - public: - AddPointCommand(SparseModel *model, - const PointType &point, - QString name = "") : - m_model(model), m_point(point), m_name(name) { } - - QString getName() const override { - return (m_name == "" ? tr("Add Point") : m_name); - } - - void execute() override { m_model->addPoint(m_point); } - void unexecute() override { m_model->deletePoint(m_point); } - - const PointType &getPoint() const { return m_point; } - - private: - SparseModel *m_model; - PointType m_point; - QString m_name; - }; - - - /** - * Command to remove a point, with undo. - */ - class DeletePointCommand : public Command - { - public: - DeletePointCommand(SparseModel *model, - const PointType &point) : - m_model(model), m_point(point) { } - - QString getName() const override { return tr("Delete Point"); } - - void execute() override { m_model->deletePoint(m_point); } - void unexecute() override { m_model->addPoint(m_point); } - - const PointType &getPoint() const { return m_point; } - - private: - SparseModel *m_model; - PointType m_point; - }; - - - /** - * Command to add or remove a series of points, with undo. - * Consecutive add/remove pairs for the same point are collapsed. - */ - class EditCommand : public MacroCommand - { - public: - EditCommand(SparseModel *model, QString commandName); - - virtual void addPoint(const PointType &point); - virtual void deletePoint(const PointType &point); - - /** - * Stack an arbitrary other command in the same sequence. - */ - void addCommand(Command *command) override { addCommand(command, true); } - - /** - * If any points have been added or deleted, return this - * command (so the caller can add it to the command history). - * Otherwise delete the command and return NULL. - */ - virtual EditCommand *finish(); - - protected: - virtual void addCommand(Command *command, bool executeFirst); - - SparseModel *m_model; - }; - - - /** - * Command to relabel a point. - */ - class RelabelCommand : public Command - { - public: - RelabelCommand(SparseModel *model, - const PointType &point, - QString newLabel) : - m_model(model), m_oldPoint(point), m_newPoint(point) { - m_newPoint.label = newLabel; - } - - QString getName() const override { return tr("Re-Label Point"); } - - void execute() override { - m_model->deletePoint(m_oldPoint); - m_model->addPoint(m_newPoint); - std::swap(m_oldPoint, m_newPoint); - } - - void unexecute() override { execute(); } - - private: - SparseModel *m_model; - PointType m_oldPoint; - PointType m_newPoint; - }; - - /** - * TabularModel methods. - */ - - int getRowCount() const override - { - return int(m_points.size()); - } - - sv_frame_t getFrameForRow(int row) const override - { - PointListConstIterator i = getPointListIteratorForRow(row); - if (i == m_points.end()) return 0; - return i->frame; - } - - int getRowForFrame(sv_frame_t frame) const override - { - if (m_rows.empty()) rebuildRowVector(); - std::vector::iterator i = - std::lower_bound(m_rows.begin(), m_rows.end(), frame); - ssize_t row = std::distance(m_rows.begin(), i); - if (i != m_rows.begin() && (i == m_rows.end() || *i != frame)) { - --row; - } - return int(row); - } - - int getColumnCount() const override { return 1; } - QVariant getData(int row, int column, int role) const override - { - PointListConstIterator i = getPointListIteratorForRow(row); - if (i == m_points.end()) { -// cerr << "no iterator for row " << row << " (have " << getRowCount() << " rows)" << endl; - return QVariant(); - } - -// cerr << "returning data for row " << row << " col " << column << endl; - - switch (column) { - case 0: { - if (role == SortRole) return int(i->frame); - RealTime rt = RealTime::frame2RealTime(i->frame, getSampleRate()); - if (role == Qt::EditRole) return rt.toString().c_str(); - else return rt.toText().c_str(); - } - case 1: return int(i->frame); - } - - return QVariant(); - } - - Command *getSetDataCommand(int row, int column, - const QVariant &value, int role) override - { - if (role != Qt::EditRole) return 0; - PointListIterator i = getPointListIteratorForRow(row); - if (i == m_points.end()) return 0; - EditCommand *command = new EditCommand(this, tr("Edit Data")); - - Point point(*i); - command->deletePoint(point); - - switch (column) { - case 0: point.frame = lrint(value.toDouble() * getSampleRate()); break; - case 1: point.frame = value.toInt(); break; - } - - command->addPoint(point); - return command->finish(); - } - - Command *getInsertRowCommand(int row) override - { - EditCommand *command = new EditCommand(this, tr("Insert Data Point")); - Point point(0); - PointListIterator i = getPointListIteratorForRow(row); - if (i == m_points.end() && i != m_points.begin()) --i; - if (i != m_points.end()) point = *i; - command->addPoint(point); - return command->finish(); - } - - Command *getRemoveRowCommand(int row) override - { - PointListIterator i = getPointListIteratorForRow(row); - if (i == m_points.end()) return 0; - EditCommand *command = new EditCommand(this, tr("Delete Data Point")); - command->deletePoint(*i); - return command->finish(); - } - -protected: - sv_samplerate_t m_sampleRate; - int m_resolution; - sv_frame_t m_extendTo; - bool m_notifyOnAdd; - sv_frame_t m_sinceLastNotifyMin; - sv_frame_t m_sinceLastNotifyMax; - bool m_hasTextLabels; - - PointList m_points; - int m_pointCount; - mutable QMutex m_mutex; - int m_completion; - - void getPointIterators(sv_frame_t frame, - PointListIterator &startItr, - PointListIterator &endItr); - void getPointIterators(sv_frame_t frame, - PointListConstIterator &startItr, - PointListConstIterator &endItr) const; - - // This is only used if the model is called on to act in - // TabularModel mode - mutable std::vector m_rows; // map from row number to frame - - void rebuildRowVector() const - { - m_rows.clear(); - for (PointListConstIterator i = m_points.begin(); i != m_points.end(); ++i) { -// std::cerr << "rebuildRowVector: row " << m_rows.size() << " -> " << i->frame << std::endl; - m_rows.push_back(i->frame); - } - } - - PointListIterator getPointListIteratorForRow(int row) - { - if (m_rows.empty()) rebuildRowVector(); - if (row < 0 || row + 1 > int(m_rows.size())) return m_points.end(); - - sv_frame_t frame = m_rows[row]; - int indexAtFrame = 0; - int ri = row; - while (ri > 0 && m_rows[ri-1] == m_rows[row]) { --ri; ++indexAtFrame; } - int initialIndexAtFrame = indexAtFrame; - - PointListIterator i0, i1; - getPointIterators(frame, i0, i1); - PointListIterator i = i0; - - for (i = i0; i != i1; ++i) { - if (i->frame < (int)frame) { continue; } - if (indexAtFrame > 0) { --indexAtFrame; continue; } - return i; - } - - if (indexAtFrame > 0) { - std::cerr << "WARNING: SparseModel::getPointListIteratorForRow: No iterator available for row " << row << " (frame = " << frame << ", index at frame = " << initialIndexAtFrame << ", leftover index " << indexAtFrame << ")" << std::endl; - } - return i; - } - - PointListConstIterator getPointListIteratorForRow(int row) const - { - if (m_rows.empty()) rebuildRowVector(); - if (row < 0 || row + 1 > int(m_rows.size())) return m_points.end(); - - sv_frame_t frame = m_rows[row]; - int indexAtFrame = 0; - int ri = row; - while (ri > 0 && m_rows[ri-1] == m_rows[row]) { --ri; ++indexAtFrame; } - int initialIndexAtFrame = indexAtFrame; - -// std::cerr << "getPointListIteratorForRow " << row << ": initialIndexAtFrame = " << initialIndexAtFrame << " for frame " << frame << std::endl; - - PointListConstIterator i0, i1; - getPointIterators(frame, i0, i1); - PointListConstIterator i = i0; - - for (i = i0; i != i1; ++i) { -// std::cerr << "i->frame is " << i->frame << ", wanting " << frame << std::endl; - - if (i->frame < (int)frame) { continue; } - if (indexAtFrame > 0) { --indexAtFrame; continue; } - return i; - } -/* - if (i == m_points.end()) { - std::cerr << "returning i at end" << std::endl; - } else { - std::cerr << "returning i with i->frame = " << i->frame << std::endl; - } -*/ - if (indexAtFrame > 0) { - std::cerr << "WARNING: SparseModel::getPointListIteratorForRow: No iterator available for row " << row << " (frame = " << frame << ", index at frame = " << initialIndexAtFrame << ", leftover index " << indexAtFrame << ")" << std::endl; - } - return i; - } - - QString toDelimitedDataStringSubsetFilled(QString delimiter, - DataExportOptions opts, - sv_frame_t f0, - sv_frame_t f1) const { - - QString s; - opts &= ~DataExportFillGaps; - - // find frame time of first point in range (if any) - sv_frame_t first = f0; - for (auto &p: m_points) { - if (p.frame >= f0) { - first = p.frame; - break; - } - } - - // project back to first frame time in range according to - // resolution. e.g. if f0 = 2, first = 9, resolution = 4 then - // we start at 5 (because 1 is too early and we need to arrive - // at 9 to match the first actual point). This method is - // stupid but easy to understand: - sv_frame_t f = first; - while (f >= f0 + m_resolution) f -= m_resolution; - - // now progress, either writing the next point (if within - // distance) or a default point - PointListConstIterator itr = m_points.begin(); - - while (f < f1) { - if (itr != m_points.end() && itr->frame <= f) { - s += itr->toDelimitedDataString(delimiter, opts, m_sampleRate); - ++itr; - } else { - s += Point(f).toDelimitedDataString(delimiter, opts, m_sampleRate); - } - s += "\n"; - f += m_resolution; - } - - return s; - } -}; - - -template -SparseModel::SparseModel(sv_samplerate_t sampleRate, - int resolution, - bool notifyOnAdd) : - m_sampleRate(sampleRate), - m_resolution(resolution), - m_extendTo(0), - m_notifyOnAdd(notifyOnAdd), - m_sinceLastNotifyMin(-1), - m_sinceLastNotifyMax(-1), - m_hasTextLabels(false), - m_pointCount(0), - m_completion(100) -{ -} - -template -sv_frame_t -SparseModel::getStartFrame() const -{ - QMutexLocker locker(&m_mutex); - sv_frame_t f = 0; - if (!m_points.empty()) { - f = m_points.begin()->frame; - } - return f; -} - -template -sv_frame_t -SparseModel::getEndFrame() const -{ - QMutexLocker locker(&m_mutex); - sv_frame_t f = 0; - if (!m_points.empty()) { - PointListConstIterator i(m_points.end()); - f = (--i)->frame + 1; - } - if (m_extendTo > f) { - return m_extendTo; - } else { - return f; - } -} - -template -bool -SparseModel::isEmpty() const -{ - return m_pointCount == 0; -} - -template -int -SparseModel::getPointCount() const -{ - return m_pointCount; -} - -template -const typename SparseModel::PointList & -SparseModel::getPoints() const -{ - return m_points; -} - -template -typename SparseModel::PointList -SparseModel::getPoints(sv_frame_t start, sv_frame_t end) const -{ - if (start > end) return PointList(); - QMutexLocker locker(&m_mutex); - - PointType startPoint(start), endPoint(end); - - PointListConstIterator startItr = m_points.lower_bound(startPoint); - PointListConstIterator endItr = m_points.upper_bound(endPoint); - - if (startItr != m_points.begin()) --startItr; - if (startItr != m_points.begin()) --startItr; - if (endItr != m_points.end()) ++endItr; - if (endItr != m_points.end()) ++endItr; - - PointList rv; - - for (PointListConstIterator i = startItr; i != endItr; ++i) { - rv.insert(*i); - } - - return rv; -} - -template -typename SparseModel::PointList -SparseModel::getPoints(sv_frame_t frame) const -{ - PointListConstIterator startItr, endItr; - getPointIterators(frame, startItr, endItr); - - PointList rv; - - for (PointListConstIterator i = startItr; i != endItr; ++i) { - rv.insert(*i); - } - - return rv; -} - -template -void -SparseModel::getPointIterators(sv_frame_t frame, - PointListIterator &startItr, - PointListIterator &endItr) -{ - QMutexLocker locker(&m_mutex); - - if (m_resolution == 0) { - startItr = m_points.end(); - endItr = m_points.end(); - return; - } - - sv_frame_t start = (frame / m_resolution) * m_resolution; - sv_frame_t end = start + m_resolution; - - PointType startPoint(start), endPoint(end); - - startItr = m_points.lower_bound(startPoint); - endItr = m_points.upper_bound(endPoint); -} - -template -void -SparseModel::getPointIterators(sv_frame_t frame, - PointListConstIterator &startItr, - PointListConstIterator &endItr) const -{ - QMutexLocker locker(&m_mutex); - - if (m_resolution == 0) { -// std::cerr << "getPointIterators: resolution == 0, returning end()" << std::endl; - startItr = m_points.end(); - endItr = m_points.end(); - return; - } - - sv_frame_t start = (frame / m_resolution) * m_resolution; - sv_frame_t end = start + m_resolution; - - PointType startPoint(start), endPoint(end); - -// std::cerr << "getPointIterators: start frame " << start << ", end frame " << end << ", m_resolution " << m_resolution << std::endl; - - startItr = m_points.lower_bound(startPoint); - endItr = m_points.upper_bound(endPoint); -} - -template -typename SparseModel::PointList -SparseModel::getPreviousPoints(sv_frame_t originFrame) const -{ - QMutexLocker locker(&m_mutex); - - PointType lookupPoint(originFrame); - PointList rv; - - PointListConstIterator i = m_points.lower_bound(lookupPoint); - if (i == m_points.begin()) return rv; - - --i; - sv_frame_t frame = i->frame; - while (i->frame == frame) { - rv.insert(*i); - if (i == m_points.begin()) break; - --i; - } - - return rv; -} - -template -typename SparseModel::PointList -SparseModel::getNextPoints(sv_frame_t originFrame) const -{ - QMutexLocker locker(&m_mutex); - - PointType lookupPoint(originFrame); - PointList rv; - - PointListConstIterator i = m_points.upper_bound(lookupPoint); - if (i == m_points.end()) return rv; - - sv_frame_t frame = i->frame; - while (i != m_points.end() && i->frame == frame) { - rv.insert(*i); - ++i; - } - - return rv; -} - -template -void -SparseModel::setResolution(int resolution) -{ - { - QMutexLocker locker(&m_mutex); - m_resolution = resolution; - m_rows.clear(); - } - emit modelChanged(); -} - -template -void -SparseModel::clear() -{ - { - QMutexLocker locker(&m_mutex); - m_points.clear(); - m_pointCount = 0; - m_rows.clear(); - } - emit modelChanged(); -} - -template -void -SparseModel::addPoint(const PointType &point) -{ - { - QMutexLocker locker(&m_mutex); - - m_points.insert(point); - m_pointCount++; - if (point.getLabel() != "") m_hasTextLabels = true; - - // Even though this model is nominally sparse, there may still - // be too many signals going on here (especially as they'll - // probably be queued from one thread to another), which is - // why we need the notifyOnAdd as an option rather than a - // necessity (the alternative is to notify on setCompletion). - - if (m_notifyOnAdd) { - m_rows.clear(); //!!! inefficient - } else { - if (m_sinceLastNotifyMin == -1 || - point.frame < m_sinceLastNotifyMin) { - m_sinceLastNotifyMin = point.frame; - } - if (m_sinceLastNotifyMax == -1 || - point.frame > m_sinceLastNotifyMax) { - m_sinceLastNotifyMax = point.frame; - } - } - } - - if (m_notifyOnAdd) { - emit modelChangedWithin(point.frame, point.frame + m_resolution); - } -} - -template -bool -SparseModel::containsPoint(const PointType &point) -{ - QMutexLocker locker(&m_mutex); - - PointListIterator i = m_points.lower_bound(point); - typename PointType::Comparator comparator; - while (i != m_points.end()) { - if (i->frame > point.frame) break; - if (!comparator(*i, point) && !comparator(point, *i)) { - return true; - } - ++i; - } - - return false; -} - -template -void -SparseModel::deletePoint(const PointType &point) -{ - { - QMutexLocker locker(&m_mutex); - - PointListIterator i = m_points.lower_bound(point); - typename PointType::Comparator comparator; - while (i != m_points.end()) { - if (i->frame > point.frame) break; - if (!comparator(*i, point) && !comparator(point, *i)) { - m_points.erase(i); - m_pointCount--; - break; - } - ++i; - } - -// std::cout << "SparseOneDimensionalModel: emit modelChanged(" -// << point.frame << ")" << std::endl; - m_rows.clear(); //!!! inefficient - } - - emit modelChangedWithin(point.frame, point.frame + m_resolution); -} - -template -void -SparseModel::setCompletion(int completion, bool update) -{ -// std::cerr << "SparseModel::setCompletion(" << completion << ")" << std::endl; - bool emitCompletionChanged = true; - bool emitGeneralModelChanged = false; - bool emitRegionChanged = false; - - { - QMutexLocker locker(&m_mutex); - - if (m_completion != completion) { - m_completion = completion; - - if (completion == 100) { - - if (m_notifyOnAdd) { - emitCompletionChanged = false; - } - - m_notifyOnAdd = true; // henceforth - m_rows.clear(); //!!! inefficient - emitGeneralModelChanged = true; - - } else if (!m_notifyOnAdd) { - - if (update && - m_sinceLastNotifyMin >= 0 && - m_sinceLastNotifyMax >= 0) { - m_rows.clear(); //!!! inefficient - emitRegionChanged = true; - } - } - } - } - - if (emitCompletionChanged) { - emit completionChanged(); - } - if (emitGeneralModelChanged) { - emit modelChanged(); - } - if (emitRegionChanged) { - emit modelChangedWithin(m_sinceLastNotifyMin, m_sinceLastNotifyMax); - m_sinceLastNotifyMin = m_sinceLastNotifyMax = -1; - } -} - -template -void -SparseModel::toXml(QTextStream &out, - QString indent, - QString extraAttributes) const -{ -// std::cerr << "SparseModel::toXml: extraAttributes = \"" -// << extraAttributes.toStdString() << std::endl; - - QString type = getXmlOutputType(); - - Model::toXml - (out, - indent, - QString("type=\"%1\" dimensions=\"%2\" resolution=\"%3\" notifyOnAdd=\"%4\" dataset=\"%5\" %6") - .arg(type) - .arg(PointType(0).getDimensions()) - .arg(m_resolution) - .arg(m_notifyOnAdd ? "true" : "false") - .arg(getObjectExportId(&m_points)) - .arg(extraAttributes)); - - out << indent; - out << QString("\n") - .arg(getObjectExportId(&m_points)) - .arg(PointType(0).getDimensions()); - - for (PointListConstIterator i = m_points.begin(); i != m_points.end(); ++i) { - i->toXml(out, indent + " "); - } - - out << indent; - out << "\n"; -} - -template -SparseModel::EditCommand::EditCommand(SparseModel *model, - QString commandName) : - MacroCommand(commandName), - m_model(model) -{ -} - -template -void -SparseModel::EditCommand::addPoint(const PointType &point) -{ - addCommand(new AddPointCommand(m_model, point), true); -} - -template -void -SparseModel::EditCommand::deletePoint(const PointType &point) -{ - addCommand(new DeletePointCommand(m_model, point), true); -} - -template -typename SparseModel::EditCommand * -SparseModel::EditCommand::finish() -{ - if (!m_commands.empty()) { - return this; - } else { - delete this; - return 0; - } -} - -template -void -SparseModel::EditCommand::addCommand(Command *command, - bool executeFirst) -{ - if (executeFirst) command->execute(); - - if (!m_commands.empty()) { - DeletePointCommand *dpc = dynamic_cast(command); - if (dpc) { - AddPointCommand *apc = dynamic_cast - (m_commands[m_commands.size() - 1]); - typename PointType::Comparator comparator; - if (apc) { - if (!comparator(apc->getPoint(), dpc->getPoint()) && - !comparator(dpc->getPoint(), apc->getPoint())) { - deleteCommand(apc); - return; - } - } - } - } - - MacroCommand::addCommand(command); -} - - -#endif - - - diff -r ab4fd193262b -r 978c143c767f data/model/SparseOneDimensionalModel.h --- a/data/model/SparseOneDimensionalModel.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/SparseOneDimensionalModel.h Fri May 17 10:02:43 2019 +0100 @@ -4,7 +4,6 @@ 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 @@ -16,110 +15,187 @@ #ifndef SV_SPARSE_ONE_DIMENSIONAL_MODEL_H #define SV_SPARSE_ONE_DIMENSIONAL_MODEL_H -#include "SparseModel.h" -#include "NoteData.h" +#include "EventCommands.h" +#include "TabularModel.h" +#include "Model.h" +#include "DeferredNotifier.h" + +#include "base/NoteData.h" +#include "base/EventSeries.h" +#include "base/NoteExportable.h" #include "base/PlayParameterRepository.h" #include "base/RealTime.h" +#include "system/System.h" + #include -struct OneDimensionalPoint -{ -public: - OneDimensionalPoint(sv_frame_t _frame) : frame(_frame) { } - OneDimensionalPoint(sv_frame_t _frame, QString _label) : frame(_frame), label(_label) { } - - int getDimensions() const { return 1; } - - sv_frame_t frame; - QString label; - - QString getLabel() const { return label; } - - void toXml(QTextStream &stream, - QString indent = "", - QString extraAttributes = "") const - { - stream << QString("%1\n") - .arg(indent).arg(frame).arg(XmlExportable::encodeEntities(label)) - .arg(extraAttributes); - } - - QString toDelimitedDataString(QString delimiter, DataExportOptions, sv_samplerate_t sampleRate) const - { - QStringList list; - list << RealTime::frame2RealTime(frame, sampleRate).toString().c_str(); - if (label != "") list << label; - return list.join(delimiter); - } - - struct Comparator { - bool operator()(const OneDimensionalPoint &p1, - const OneDimensionalPoint &p2) const { - if (p1.frame != p2.frame) return p1.frame < p2.frame; - return p1.label < p2.label; - } - }; - - struct OrderComparator { - bool operator()(const OneDimensionalPoint &p1, - const OneDimensionalPoint &p2) const { - return p1.frame < p2.frame; - } - }; -}; - - -class SparseOneDimensionalModel : public SparseModel, +/** + * A model representing a series of time instants with optional labels + * but without values. + */ +class SparseOneDimensionalModel : public Model, + public TabularModel, + public EventEditable, public NoteExportable { Q_OBJECT public: - SparseOneDimensionalModel(sv_samplerate_t sampleRate, int resolution, + SparseOneDimensionalModel(sv_samplerate_t sampleRate, + int resolution, bool notifyOnAdd = true) : - SparseModel(sampleRate, resolution, notifyOnAdd) - { + m_sampleRate(sampleRate), + m_resolution(resolution), + m_haveTextLabels(false), + m_notifier(this, + notifyOnAdd ? + DeferredNotifier::NOTIFY_ALWAYS : + DeferredNotifier::NOTIFY_DEFERRED), + m_completion(100) { PlayParameterRepository::getInstance()->addPlayable(this); } - virtual ~SparseOneDimensionalModel() - { + virtual ~SparseOneDimensionalModel() { PlayParameterRepository::getInstance()->removePlayable(this); } + QString getTypeName() const override { return tr("Sparse 1-D"); } + bool isSparse() const override { return true; } + bool isOK() const override { return true; } + + sv_frame_t getStartFrame() const override { + return m_events.getStartFrame(); + } + sv_frame_t getEndFrame() const override { + if (m_events.isEmpty()) return 0; + sv_frame_t e = m_events.getEndFrame() + 1; + if (e % m_resolution == 0) return e; + else return (e / m_resolution + 1) * m_resolution; + } + + sv_samplerate_t getSampleRate() const override { return m_sampleRate; } + int getResolution() const { return m_resolution; } + bool canPlay() const override { return true; } + QString getDefaultPlayClipId() const override { return "tap"; } + + bool hasTextLabels() const { return m_haveTextLabels; } + + int getCompletion() const override { return m_completion; } - QString getDefaultPlayClipId() const override - { - return "tap"; + void setCompletion(int completion, bool update = true) { + + { QMutexLocker locker(&m_mutex); + if (m_completion == completion) return; + m_completion = completion; + } + + if (update) { + m_notifier.makeDeferredNotifications(); + } + + emit completionChanged(); + + if (completion == 100) { + // henceforth: + m_notifier.switchMode(DeferredNotifier::NOTIFY_ALWAYS); + emit modelChanged(); + } + } + + /** + * Query methods. + */ + + int getEventCount() const { + return m_events.count(); + } + bool isEmpty() const { + return m_events.isEmpty(); + } + bool containsEvent(const Event &e) const { + return m_events.contains(e); + } + EventVector getAllEvents() const { + return m_events.getAllEvents(); + } + EventVector getEventsSpanning(sv_frame_t f, sv_frame_t duration) const { + return m_events.getEventsSpanning(f, duration); + } + EventVector getEventsCovering(sv_frame_t f) const { + return m_events.getEventsCovering(f); + } + EventVector getEventsWithin(sv_frame_t f, sv_frame_t duration, + int overspill = 0) const { + return m_events.getEventsWithin(f, duration, overspill); + } + EventVector getEventsStartingWithin(sv_frame_t f, sv_frame_t duration) const { + return m_events.getEventsStartingWithin(f, duration); + } + EventVector getEventsStartingAt(sv_frame_t f) const { + return m_events.getEventsStartingAt(f); + } + bool getNearestEventMatching(sv_frame_t startSearchAt, + std::function predicate, + EventSeries::Direction direction, + Event &found) const { + return m_events.getNearestEventMatching + (startSearchAt, predicate, direction, found); } - int getIndexOf(const Point &point) - { - // slow - int i = 0; - Point::Comparator comparator; - for (PointList::const_iterator j = m_points.begin(); - j != m_points.end(); ++j, ++i) { - if (!comparator(*j, point) && !comparator(point, *j)) return i; + /** + * Editing methods. + */ + void add(Event e) override { + + { QMutexLocker locker(&m_mutex); + m_events.add(e.withoutValue().withoutDuration()); + + if (e.getLabel() != "") { + m_haveTextLabels = true; + } } - return -1; + + m_notifier.update(e.getFrame(), m_resolution); } - - QString getTypeName() const override { return tr("Sparse 1-D"); } - + + void remove(Event e) override { + { QMutexLocker locker(&m_mutex); + m_events.remove(e); + } + emit modelChangedWithin(e.getFrame(), e.getFrame() + m_resolution); + } + /** * TabularModel methods. */ - int getColumnCount() const override - { + int getRowCount() const override { + return m_events.count(); + } + + int getColumnCount() const override { return 3; } - QString getHeading(int column) const override - { + bool isColumnTimeValue(int column) const override { + return (column < 2); + } + + sv_frame_t getFrameForRow(int row) const override { + if (row < 0 || row >= m_events.count()) { + return 0; + } + Event e = m_events.getEventByIndex(row); + return e.getFrame(); + } + + int getRowForFrame(sv_frame_t frame) const override { + return m_events.getIndexForEvent(Event(frame)); + } + + QString getHeading(int column) const override { switch (column) { case 0: return tr("Time"); case 1: return tr("Frame"); @@ -128,82 +204,117 @@ } } - QVariant getData(int row, int column, int role) const override - { - if (column < 2) { - return SparseModel::getData - (row, column, role); + SortType getSortType(int column) const override { + if (column == 2) return SortAlphabetical; + return SortNumeric; + } + + QVariant getData(int row, int column, int role) const override { + + if (row < 0 || row >= m_events.count()) { + return QVariant(); } - PointListConstIterator i = getPointListIteratorForRow(row); - if (i == m_points.end()) return QVariant(); + Event e = m_events.getEventByIndex(row); switch (column) { - case 2: return i->label; + case 0: return adaptFrameForRole(e.getFrame(), getSampleRate(), role); + case 1: return int(e.getFrame()); + case 2: return e.getLabel(); default: return QVariant(); } } - Command *getSetDataCommand(int row, int column, const QVariant &value, int role) override - { - if (column < 2) { - return SparseModel::getSetDataCommand - (row, column, value, role); + Command *getSetDataCommand(int row, int column, const QVariant &value, int role) override { + if (row < 0 || row >= m_events.count()) return nullptr; + if (role != Qt::EditRole) return nullptr; + + Event e0 = m_events.getEventByIndex(row); + Event e1; + + switch (column) { + case 0: e1 = e0.withFrame(sv_frame_t(round(value.toDouble() * + getSampleRate()))); break; + case 1: e1 = e0.withFrame(value.toInt()); break; + case 2: e1 = e0.withLabel(value.toString()); break; } - if (role != Qt::EditRole) return 0; - PointListConstIterator i = getPointListIteratorForRow(row); - if (i == m_points.end()) return 0; - EditCommand *command = new EditCommand(this, tr("Edit Data")); - - Point point(*i); - command->deletePoint(point); - - switch (column) { - case 2: point.label = value.toString(); break; - } - - command->addPoint(point); + ChangeEventsCommand *command = + new ChangeEventsCommand(this, tr("Edit Data")); + command->remove(e0); + command->add(e1); return command->finish(); } - - bool isColumnTimeValue(int column) const override - { - return (column < 2); - } - - SortType getSortType(int column) const override - { - if (column == 2) return SortAlphabetical; - return SortNumeric; - } - /** * NoteExportable methods. */ NoteList getNotes() const override { - return getNotesWithin(getStartFrame(), getEndFrame()); + return getNotesStartingWithin(getStartFrame(), + getEndFrame() - getStartFrame()); } - NoteList getNotesWithin(sv_frame_t startFrame, sv_frame_t endFrame) const override { + NoteList getNotesActiveAt(sv_frame_t frame) const override { + return getNotesStartingWithin(frame, 1); + } + + NoteList getNotesStartingWithin(sv_frame_t startFrame, + sv_frame_t duration) const override { - PointList points = getPoints(startFrame, endFrame); NoteList notes; - - for (PointList::iterator pli = - points.begin(); pli != points.end(); ++pli) { - - notes.push_back - (NoteData(pli->frame, - sv_frame_t(getSampleRate() / 6), // arbitrary short duration - 64, // default pitch - 100)); // default velocity + EventVector ee = m_events.getEventsStartingWithin(startFrame, duration); + for (const auto &e: ee) { + notes.push_back(e.toNoteData(getSampleRate(), true)); } - return notes; } + + /** + * XmlExportable methods. + */ + void toXml(QTextStream &out, + QString indent = "", + QString extraAttributes = "") const override { + + Model::toXml + (out, + indent, + QString("type=\"sparse\" dimensions=\"1\" resolution=\"%1\" " + "notifyOnAdd=\"%2\" dataset=\"%3\" %4") + .arg(m_resolution) + .arg("true") // always true after model reaches 100% - + // subsequent events are always notified + .arg(m_events.getExportId()) + .arg(extraAttributes)); + + m_events.toXml(out, indent, QString("dimensions=\"1\"")); + } + + QString toDelimitedDataString(QString delimiter, + DataExportOptions options, + sv_frame_t startFrame, + sv_frame_t duration) const override { + return m_events.toDelimitedDataString(delimiter, + options, + startFrame, + duration, + m_sampleRate, + m_resolution, + Event()); + } + +protected: + sv_samplerate_t m_sampleRate; + int m_resolution; + + bool m_haveTextLabels; + DeferredNotifier m_notifier; + int m_completion; + + EventSeries m_events; + + mutable QMutex m_mutex; }; #endif diff -r ab4fd193262b -r 978c143c767f data/model/SparseTimeValueModel.h --- a/data/model/SparseTimeValueModel.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/SparseTimeValueModel.h Fri May 17 10:02:43 2019 +0100 @@ -4,7 +4,6 @@ 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 @@ -16,76 +15,43 @@ #ifndef SV_SPARSE_TIME_VALUE_MODEL_H #define SV_SPARSE_TIME_VALUE_MODEL_H -#include "SparseValueModel.h" +#include "EventCommands.h" +#include "TabularModel.h" +#include "Model.h" +#include "DeferredNotifier.h" + +#include "base/RealTime.h" +#include "base/EventSeries.h" +#include "base/UnitDatabase.h" #include "base/PlayParameterRepository.h" -#include "base/RealTime.h" + +#include "system/System.h" /** - * Time/value point type for use in a SparseModel or SparseValueModel. - * With this point type, the model basically represents a wiggly-line - * plot with points at arbitrary intervals of the model resolution. + * A model representing a wiggly-line plot with points at arbitrary + * intervals of the model resolution. */ - -struct TimeValuePoint -{ -public: - TimeValuePoint(sv_frame_t _frame) : frame(_frame), value(0.0f) { } - TimeValuePoint(sv_frame_t _frame, float _value, QString _label) : - frame(_frame), value(_value), label(_label) { } - - int getDimensions() const { return 2; } - - sv_frame_t frame; - float value; - QString label; - - QString getLabel() const { return label; } - - void toXml(QTextStream &stream, QString indent = "", - QString extraAttributes = "") const - { - stream << QString("%1\n") - .arg(indent).arg(frame).arg(value).arg(XmlExportable::encodeEntities(label)) - .arg(extraAttributes); - } - - QString toDelimitedDataString(QString delimiter, DataExportOptions, sv_samplerate_t sampleRate) const - { - QStringList list; - list << RealTime::frame2RealTime(frame, sampleRate).toString().c_str(); - list << QString("%1").arg(value); - if (label != "") list << label; - return list.join(delimiter); - } - - struct Comparator { - bool operator()(const TimeValuePoint &p1, - const TimeValuePoint &p2) const { - if (p1.frame != p2.frame) return p1.frame < p2.frame; - if (p1.value != p2.value) return p1.value < p2.value; - return p1.label < p2.label; - } - }; - - struct OrderComparator { - bool operator()(const TimeValuePoint &p1, - const TimeValuePoint &p2) const { - return p1.frame < p2.frame; - } - }; -}; - - -class SparseTimeValueModel : public SparseValueModel +class SparseTimeValueModel : public Model, + public TabularModel, + public EventEditable { Q_OBJECT public: - SparseTimeValueModel(sv_samplerate_t sampleRate, int resolution, + SparseTimeValueModel(sv_samplerate_t sampleRate, + int resolution, bool notifyOnAdd = true) : - SparseValueModel(sampleRate, resolution, - notifyOnAdd) - { + m_sampleRate(sampleRate), + m_resolution(resolution), + m_valueMinimum(0.f), + m_valueMaximum(0.f), + m_haveExtents(false), + m_haveTextLabels(false), + m_notifier(this, + notifyOnAdd ? + DeferredNotifier::NOTIFY_ALWAYS : + DeferredNotifier::NOTIFY_DEFERRED), + m_completion(100) { // Model is playable, but may not sound (if units not Hz or // range unsuitable) PlayParameterRepository::getInstance()->addPlayable(this); @@ -94,36 +60,189 @@ SparseTimeValueModel(sv_samplerate_t sampleRate, int resolution, float valueMinimum, float valueMaximum, bool notifyOnAdd = true) : - SparseValueModel(sampleRate, resolution, - valueMinimum, valueMaximum, - notifyOnAdd) - { + m_sampleRate(sampleRate), + m_resolution(resolution), + m_valueMinimum(valueMinimum), + m_valueMaximum(valueMaximum), + m_haveExtents(true), + m_haveTextLabels(false), + m_notifier(this, + notifyOnAdd ? + DeferredNotifier::NOTIFY_ALWAYS : + DeferredNotifier::NOTIFY_DEFERRED), + m_completion(100) { // Model is playable, but may not sound (if units not Hz or // range unsuitable) PlayParameterRepository::getInstance()->addPlayable(this); } - virtual ~SparseTimeValueModel() - { + virtual ~SparseTimeValueModel() { PlayParameterRepository::getInstance()->removePlayable(this); } QString getTypeName() const override { return tr("Sparse Time-Value"); } + bool isSparse() const override { return true; } + bool isOK() const override { return true; } + + sv_frame_t getStartFrame() const override { + return m_events.getStartFrame(); + } + sv_frame_t getEndFrame() const override { + if (m_events.isEmpty()) return 0; + sv_frame_t e = m_events.getEndFrame() + 1; + if (e % m_resolution == 0) return e; + else return (e / m_resolution + 1) * m_resolution; + } + + sv_samplerate_t getSampleRate() const override { return m_sampleRate; } + int getResolution() const { return m_resolution; } bool canPlay() const override { return true; } bool getDefaultPlayAudible() const override { return false; } // user must unmute + QString getScaleUnits() const { return m_units; } + void setScaleUnits(QString units) { + m_units = units; + UnitDatabase::getInstance()->registerUnit(units); + } + + bool hasTextLabels() const { return m_haveTextLabels; } + + float getValueMinimum() const { return m_valueMinimum; } + float getValueMaximum() const { return m_valueMaximum; } + + int getCompletion() const override { return m_completion; } + + void setCompletion(int completion, bool update = true) { + + { QMutexLocker locker(&m_mutex); + if (m_completion == completion) return; + m_completion = completion; + } + + if (update) { + m_notifier.makeDeferredNotifications(); + } + + emit completionChanged(); + + if (completion == 100) { + // henceforth: + m_notifier.switchMode(DeferredNotifier::NOTIFY_ALWAYS); + emit modelChanged(); + } + } + + /** + * Query methods. + */ + + int getEventCount() const { + return m_events.count(); + } + bool isEmpty() const { + return m_events.isEmpty(); + } + bool containsEvent(const Event &e) const { + return m_events.contains(e); + } + EventVector getAllEvents() const { + return m_events.getAllEvents(); + } + EventVector getEventsSpanning(sv_frame_t f, sv_frame_t duration) const { + return m_events.getEventsSpanning(f, duration); + } + EventVector getEventsCovering(sv_frame_t f) const { + return m_events.getEventsCovering(f); + } + EventVector getEventsWithin(sv_frame_t f, sv_frame_t duration, + int overspill = 0) const { + return m_events.getEventsWithin(f, duration, overspill); + } + EventVector getEventsStartingWithin(sv_frame_t f, sv_frame_t duration) const { + return m_events.getEventsStartingWithin(f, duration); + } + EventVector getEventsStartingAt(sv_frame_t f) const { + return m_events.getEventsStartingAt(f); + } + bool getNearestEventMatching(sv_frame_t startSearchAt, + std::function predicate, + EventSeries::Direction direction, + Event &found) const { + return m_events.getNearestEventMatching + (startSearchAt, predicate, direction, found); + } + + /** + * Editing methods. + */ + void add(Event e) override { + + bool allChange = false; + + { QMutexLocker locker(&m_mutex); + m_events.add(e.withoutDuration()); // can't have duration here + + if (e.getLabel() != "") { + m_haveTextLabels = true; + } + + float v = e.getValue(); + if (!ISNAN(v) && !ISINF(v)) { + if (!m_haveExtents || v < m_valueMinimum) { + m_valueMinimum = v; allChange = true; + } + if (!m_haveExtents || v > m_valueMaximum) { + m_valueMaximum = v; allChange = true; + } + m_haveExtents = true; + } + } + + m_notifier.update(e.getFrame(), m_resolution); + + if (allChange) { + emit modelChanged(); + } + } + + void remove(Event e) override { + { + QMutexLocker locker(&m_mutex); + m_events.remove(e); + } + emit modelChangedWithin(e.getFrame(), e.getFrame() + m_resolution); + } + /** * TabularModel methods. */ - int getColumnCount() const override - { + int getRowCount() const override { + return m_events.count(); + } + + int getColumnCount() const override { return 4; } - QString getHeading(int column) const override - { + bool isColumnTimeValue(int column) const override { + return (column < 2); + } + + sv_frame_t getFrameForRow(int row) const override { + if (row < 0 || row >= m_events.count()) { + return 0; + } + Event e = m_events.getEventByIndex(row); + return e.getFrame(); + } + + int getRowForFrame(sv_frame_t frame) const override { + return m_events.getIndexForEvent(Event(frame)); + } + + QString getHeading(int column) const override { switch (column) { case 0: return tr("Time"); case 1: return tr("Frame"); @@ -133,59 +252,104 @@ } } - QVariant getData(int row, int column, int role) const override - { - if (column < 2) { - return SparseValueModel::getData - (row, column, role); + SortType getSortType(int column) const override { + if (column == 3) return SortAlphabetical; + return SortNumeric; + } + + QVariant getData(int row, int column, int role) const override { + + if (row < 0 || row >= m_events.count()) { + return QVariant(); } - PointListConstIterator i = getPointListIteratorForRow(row); - if (i == m_points.end()) return QVariant(); + Event e = m_events.getEventByIndex(row); switch (column) { - case 2: - if (role == Qt::EditRole || role == SortRole) return i->value; - else return QString("%1 %2").arg(i->value).arg(getScaleUnits()); - case 3: return i->label; + case 0: return adaptFrameForRole(e.getFrame(), getSampleRate(), role); + case 1: return int(e.getFrame()); + case 2: return adaptValueForRole(e.getValue(), getScaleUnits(), role); + case 3: return e.getLabel(); default: return QVariant(); } } - Command *getSetDataCommand(int row, int column, const QVariant &value, int role) override - { - if (column < 2) { - return SparseValueModel::getSetDataCommand - (row, column, value, role); + Command *getSetDataCommand(int row, int column, const QVariant &value, int role) override { + if (row < 0 || row >= m_events.count()) return nullptr; + if (role != Qt::EditRole) return nullptr; + + Event e0 = m_events.getEventByIndex(row); + Event e1; + + switch (column) { + case 0: e1 = e0.withFrame(sv_frame_t(round(value.toDouble() * + getSampleRate()))); break; + case 1: e1 = e0.withFrame(value.toInt()); break; + case 2: e1 = e0.withValue(float(value.toDouble())); break; + case 3: e1 = e0.withLabel(value.toString()); break; } - if (role != Qt::EditRole) return 0; - PointListConstIterator i = getPointListIteratorForRow(row); - if (i == m_points.end()) return 0; - EditCommand *command = new EditCommand(this, tr("Edit Data")); - - Point point(*i); - command->deletePoint(point); - - switch (column) { - case 2: point.value = float(value.toDouble()); break; - case 3: point.label = value.toString(); break; - } - - command->addPoint(point); + ChangeEventsCommand *command = + new ChangeEventsCommand(this, tr("Edit Data")); + command->remove(e0); + command->add(e1); return command->finish(); } + + /** + * XmlExportable methods. + */ + void toXml(QTextStream &out, + QString indent = "", + QString extraAttributes = "") const override { - bool isColumnTimeValue(int column) const override - { - return (column < 2); + Model::toXml + (out, + indent, + QString("type=\"sparse\" dimensions=\"2\" resolution=\"%1\" " + "notifyOnAdd=\"%2\" dataset=\"%3\" " + "minimum=\"%4\" maximum=\"%5\" " + "units=\"%6\" %7") + .arg(m_resolution) + .arg("true") // always true after model reaches 100% - + // subsequent events are always notified + .arg(m_events.getExportId()) + .arg(m_valueMinimum) + .arg(m_valueMaximum) + .arg(encodeEntities(m_units)) + .arg(extraAttributes)); + + m_events.toXml(out, indent, QString("dimensions=\"2\"")); } - SortType getSortType(int column) const override - { - if (column == 3) return SortAlphabetical; - return SortNumeric; + QString toDelimitedDataString(QString delimiter, + DataExportOptions options, + sv_frame_t startFrame, + sv_frame_t duration) const override { + return m_events.toDelimitedDataString(delimiter, + options, + startFrame, + duration, + m_sampleRate, + m_resolution, + Event().withValue(0.f)); } + +protected: + sv_samplerate_t m_sampleRate; + int m_resolution; + + float m_valueMinimum; + float m_valueMaximum; + bool m_haveExtents; + bool m_haveTextLabels; + QString m_units; + DeferredNotifier m_notifier; + int m_completion; + + EventSeries m_events; + + mutable QMutex m_mutex; }; diff -r ab4fd193262b -r 978c143c767f data/model/SparseValueModel.h --- a/data/model/SparseValueModel.h Thu May 16 12:54:58 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,140 +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_SPARSE_VALUE_MODEL_H -#define SV_SPARSE_VALUE_MODEL_H - -#include "SparseModel.h" -#include "base/UnitDatabase.h" - -#include "system/System.h" - -/** - * Model containing sparse data (points with some properties) of which - * one of the properties is an arbitrary float value. The other - * properties depend on the point type. - */ - -template -class SparseValueModel : public SparseModel -{ -public: - SparseValueModel(sv_samplerate_t sampleRate, int resolution, - bool notifyOnAdd = true) : - SparseModel(sampleRate, resolution, notifyOnAdd), - m_valueMinimum(0.f), - m_valueMaximum(0.f), - m_haveExtents(false) - { } - - SparseValueModel(sv_samplerate_t sampleRate, int resolution, - float valueMinimum, float valueMaximum, - bool notifyOnAdd = true) : - SparseModel(sampleRate, resolution, notifyOnAdd), - m_valueMinimum(valueMinimum), - m_valueMaximum(valueMaximum), - m_haveExtents(true) - { } - - using SparseModel::m_points; - using SparseModel::modelChanged; - using SparseModel::getPoints; - using SparseModel::tr; - - QString getTypeName() const override { return tr("Sparse Value"); } - - virtual float getValueMinimum() const { return m_valueMinimum; } - virtual float getValueMaximum() const { return m_valueMaximum; } - - virtual QString getScaleUnits() const { return m_units; } - virtual void setScaleUnits(QString units) { - m_units = units; - UnitDatabase::getInstance()->registerUnit(units); - } - - void addPoint(const PointType &point) override - { - bool allChange = false; - - if (!ISNAN(point.value) && !ISINF(point.value)) { - if (!m_haveExtents || point.value < m_valueMinimum) { - m_valueMinimum = point.value; allChange = true; -// std::cerr << "addPoint: value min = " << m_valueMinimum << std::endl; - } - if (!m_haveExtents || point.value > m_valueMaximum) { - m_valueMaximum = point.value; allChange = true; -// std::cerr << "addPoint: value max = " << m_valueMaximum << " (min = " << m_valueMinimum << ")" << std::endl; - } - m_haveExtents = true; - } - - SparseModel::addPoint(point); - if (allChange) emit modelChanged(); - } - - void deletePoint(const PointType &point) override - { - SparseModel::deletePoint(point); - - if (point.value == m_valueMinimum || - point.value == m_valueMaximum) { - - float formerMin = m_valueMinimum, formerMax = m_valueMaximum; - - for (typename SparseModel::PointList::const_iterator i - = m_points.begin(); - i != m_points.end(); ++i) { - - if (i == m_points.begin() || i->value < m_valueMinimum) { - m_valueMinimum = i->value; -// std::cerr << "deletePoint: value min = " << m_valueMinimum << std::endl; - } - if (i == m_points.begin() || i->value > m_valueMaximum) { - m_valueMaximum = i->value; -// std::cerr << "deletePoint: value max = " << m_valueMaximum << std::endl; - } - } - - if (formerMin != m_valueMinimum || formerMax != m_valueMaximum) { - emit modelChanged(); - } - } - } - - void toXml(QTextStream &stream, - QString indent = "", - QString extraAttributes = "") const override - { - std::cerr << "SparseValueModel::toXml: extraAttributes = \"" - << extraAttributes.toStdString() << std::endl; - - SparseModel::toXml - (stream, - indent, - QString("%1 minimum=\"%2\" maximum=\"%3\" units=\"%4\"") - .arg(extraAttributes).arg(m_valueMinimum).arg(m_valueMaximum) - .arg(this->encodeEntities(m_units))); - } - -protected: - float m_valueMinimum; - float m_valueMaximum; - bool m_haveExtents; - QString m_units; -}; - - -#endif - diff -r ab4fd193262b -r 978c143c767f data/model/TabularModel.h --- a/data/model/TabularModel.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/TabularModel.h Fri May 17 10:02:43 2019 +0100 @@ -19,6 +19,8 @@ #include #include +#include "base/RealTime.h" + class Command; /** @@ -55,6 +57,22 @@ virtual Command *getSetDataCommand(int /* row */, int /* column */, const QVariant &, int /* role */) { return 0; } virtual Command *getInsertRowCommand(int /* beforeRow */) { return 0; } virtual Command *getRemoveRowCommand(int /* row */) { return 0; } + + QVariant adaptFrameForRole(sv_frame_t frame, + sv_samplerate_t rate, + int role) const { + if (role == SortRole) return int(frame); + RealTime rt = RealTime::frame2RealTime(frame, rate); + if (role == Qt::EditRole) return rt.toString().c_str(); + else return rt.toText().c_str(); + } + + QVariant adaptValueForRole(float value, + QString unit, + int role) const { + if (role == SortRole || role == Qt::EditRole) return value; + else return QString("%1 %2").arg(value).arg(unit); + } }; #endif diff -r ab4fd193262b -r 978c143c767f data/model/TextModel.h --- a/data/model/TextModel.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/TextModel.h Fri May 17 10:02:43 2019 +0100 @@ -4,7 +4,6 @@ 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 @@ -16,103 +15,170 @@ #ifndef SV_TEXT_MODEL_H #define SV_TEXT_MODEL_H -#include "SparseModel.h" +#include "EventCommands.h" +#include "TabularModel.h" +#include "Model.h" +#include "DeferredNotifier.h" + +#include "base/EventSeries.h" #include "base/XmlExportable.h" #include "base/RealTime.h" +#include "system/System.h" + #include /** - * Text point type for use in a SparseModel. This represents a piece - * of text at a given time and y-value in the [0,1) range (indicative - * of height on the window). Intended for casual textual annotations. + * A model representing casual textual annotations. A piece of text + * has a given time and y-value in the [0,1) range (indicative of + * height on the window). */ - -struct TextPoint : public XmlExportable -{ -public: - TextPoint(sv_frame_t _frame) : frame(_frame), height(0.0f) { } - TextPoint(sv_frame_t _frame, float _height, QString _label) : - frame(_frame), height(_height), label(_label) { } - - int getDimensions() const { return 2; } - - sv_frame_t frame; - float height; - QString label; - - QString getLabel() const { return label; } - - void toXml(QTextStream &stream, QString indent = "", - QString extraAttributes = "") const override - { - stream << QString("%1\n") - .arg(indent).arg(frame).arg(height) - .arg(encodeEntities(label)).arg(extraAttributes); - } - - QString toDelimitedDataString(QString delimiter, DataExportOptions, sv_samplerate_t sampleRate) const - { - QStringList list; - list << RealTime::frame2RealTime(frame, sampleRate).toString().c_str(); - list << QString("%1").arg(height); - if (label != "") list << label; - return list.join(delimiter); - } - - struct Comparator { - bool operator()(const TextPoint &p1, - const TextPoint &p2) const { - if (p1.frame != p2.frame) return p1.frame < p2.frame; - if (p1.height != p2.height) return p1.height < p2.height; - return p1.label < p2.label; - } - }; - - struct OrderComparator { - bool operator()(const TextPoint &p1, - const TextPoint &p2) const { - return p1.frame < p2.frame; - } - }; -}; - - -// Make this a class rather than a typedef so it can be predeclared. - -class TextModel : public SparseModel +class TextModel : public Model, + public TabularModel, + public EventEditable { Q_OBJECT public: - TextModel(sv_samplerate_t sampleRate, int resolution, bool notifyOnAdd = true) : - SparseModel(sampleRate, resolution, notifyOnAdd) - { } - - void toXml(QTextStream &out, - QString indent = "", - QString extraAttributes = "") const override - { - SparseModel::toXml - (out, - indent, - QString("%1 subtype=\"text\"") - .arg(extraAttributes)); + TextModel(sv_samplerate_t sampleRate, + int resolution, + bool notifyOnAdd = true) : + m_sampleRate(sampleRate), + m_resolution(resolution), + m_notifier(this, + notifyOnAdd ? + DeferredNotifier::NOTIFY_ALWAYS : + DeferredNotifier::NOTIFY_DEFERRED), + m_completion(100) { } QString getTypeName() const override { return tr("Text"); } + bool isSparse() const override { return true; } + bool isOK() const override { return true; } + + sv_frame_t getStartFrame() const override { + return m_events.getStartFrame(); + } + sv_frame_t getEndFrame() const override { + if (m_events.isEmpty()) return 0; + sv_frame_t e = m_events.getEndFrame() + 1; + if (e % m_resolution == 0) return e; + else return (e / m_resolution + 1) * m_resolution; + } + + sv_samplerate_t getSampleRate() const override { return m_sampleRate; } + int getResolution() const { return m_resolution; } + + int getCompletion() const override { return m_completion; } + + void setCompletion(int completion, bool update = true) { + + { QMutexLocker locker(&m_mutex); + if (m_completion == completion) return; + m_completion = completion; + } + + if (update) { + m_notifier.makeDeferredNotifications(); + } + + emit completionChanged(); + + if (completion == 100) { + // henceforth: + m_notifier.switchMode(DeferredNotifier::NOTIFY_ALWAYS); + emit modelChanged(); + } + } + + /** + * Query methods. + */ + + int getEventCount() const { + return m_events.count(); + } + bool isEmpty() const { + return m_events.isEmpty(); + } + bool containsEvent(const Event &e) const { + return m_events.contains(e); + } + EventVector getAllEvents() const { + return m_events.getAllEvents(); + } + EventVector getEventsSpanning(sv_frame_t f, sv_frame_t duration) const { + return m_events.getEventsSpanning(f, duration); + } + EventVector getEventsCovering(sv_frame_t f) const { + return m_events.getEventsCovering(f); + } + EventVector getEventsWithin(sv_frame_t f, sv_frame_t duration, + int overspill = 0) const { + return m_events.getEventsWithin(f, duration, overspill); + } + EventVector getEventsStartingWithin(sv_frame_t f, sv_frame_t duration) const { + return m_events.getEventsStartingWithin(f, duration); + } + EventVector getEventsStartingAt(sv_frame_t f) const { + return m_events.getEventsStartingAt(f); + } + bool getNearestEventMatching(sv_frame_t startSearchAt, + std::function predicate, + EventSeries::Direction direction, + Event &found) const { + return m_events.getNearestEventMatching + (startSearchAt, predicate, direction, found); + } + + /** + * Editing methods. + */ + void add(Event e) override { + + { QMutexLocker locker(&m_mutex); + m_events.add(e.withoutDuration().withoutLevel()); + } + + m_notifier.update(e.getFrame(), m_resolution); + } + + void remove(Event e) override { + { QMutexLocker locker(&m_mutex); + m_events.remove(e); + } + emit modelChangedWithin(e.getFrame(), e.getFrame() + m_resolution); + } /** * TabularModel methods. */ - int getColumnCount() const override - { + int getRowCount() const override { + return m_events.count(); + } + + int getColumnCount() const override { return 4; } - QString getHeading(int column) const override - { + bool isColumnTimeValue(int column) const override { + return (column < 2); + } + + sv_frame_t getFrameForRow(int row) const override { + if (row < 0 || row >= m_events.count()) { + return 0; + } + Event e = m_events.getEventByIndex(row); + return e.getFrame(); + } + + int getRowForFrame(sv_frame_t frame) const override { + return m_events.getIndexForEvent(Event(frame)); + } + + QString getHeading(int column) const override { switch (column) { case 0: return tr("Time"); case 1: return tr("Frame"); @@ -122,57 +188,98 @@ } } - QVariant getData(int row, int column, int role) const override - { - if (column < 2) { - return SparseModel::getData - (row, column, role); + SortType getSortType(int column) const override { + if (column == 3) return SortAlphabetical; + return SortNumeric; + } + + QVariant getData(int row, int column, int role) const override { + + if (row < 0 || row >= m_events.count()) { + return QVariant(); } - PointListConstIterator i = getPointListIteratorForRow(row); - if (i == m_points.end()) return QVariant(); + Event e = m_events.getEventByIndex(row); switch (column) { - case 2: return i->height; - case 3: return i->label; + case 0: return adaptFrameForRole(e.getFrame(), getSampleRate(), role); + case 1: return int(e.getFrame()); + case 2: return e.getValue(); + case 3: return e.getLabel(); default: return QVariant(); } } - Command *getSetDataCommand(int row, int column, const QVariant &value, int role) override - { - if (column < 2) { - return SparseModel::getSetDataCommand - (row, column, value, role); + Command *getSetDataCommand(int row, int column, const QVariant &value, int role) override { + + if (row < 0 || row >= m_events.count()) return nullptr; + if (role != Qt::EditRole) return nullptr; + + Event e0 = m_events.getEventByIndex(row); + Event e1; + + switch (column) { + case 0: e1 = e0.withFrame(sv_frame_t(round(value.toDouble() * + getSampleRate()))); break; + case 1: e1 = e0.withFrame(value.toInt()); break; + case 2: e1 = e0.withValue(float(value.toDouble())); break; + case 3: e1 = e0.withLabel(value.toString()); break; } - if (role != Qt::EditRole) return 0; - PointListIterator i = getPointListIteratorForRow(row); - if (i == m_points.end()) return 0; - EditCommand *command = new EditCommand(this, tr("Edit Data")); - - Point point(*i); - command->deletePoint(point); - - switch (column) { - case 2: point.height = float(value.toDouble()); break; - case 3: point.label = value.toString(); break; - } - - command->addPoint(point); + ChangeEventsCommand *command = + new ChangeEventsCommand(this, tr("Edit Data")); + command->remove(e0); + command->add(e1); return command->finish(); } + + /** + * XmlExportable methods. + */ + void toXml(QTextStream &out, + QString indent = "", + QString extraAttributes = "") const override { - bool isColumnTimeValue(int column) const override - { - return (column < 2); + Model::toXml + (out, + indent, + QString("type=\"sparse\" dimensions=\"2\" resolution=\"%1\" " + "notifyOnAdd=\"%2\" dataset=\"%3\" subtype=\"text\" %4") + .arg(m_resolution) + .arg("true") // always true after model reaches 100% - + // subsequent events are always notified + .arg(m_events.getExportId()) + .arg(extraAttributes)); + + Event::ExportNameOptions options; + options.valueAtttributeName = "height"; + + m_events.toXml(out, indent, QString("dimensions=\"2\""), options); } - SortType getSortType(int column) const override - { - if (column == 3) return SortAlphabetical; - return SortNumeric; + QString toDelimitedDataString(QString delimiter, + DataExportOptions options, + sv_frame_t startFrame, + sv_frame_t duration) const override { + return m_events.toDelimitedDataString(delimiter, + options, + startFrame, + duration, + m_sampleRate, + m_resolution, + Event().withValue(0.f)); } + +protected: + sv_samplerate_t m_sampleRate; + int m_resolution; + + DeferredNotifier m_notifier; + int m_completion; + + EventSeries m_events; + + mutable QMutex m_mutex; }; diff -r ab4fd193262b -r 978c143c767f data/model/WritableWaveFileModel.cpp --- a/data/model/WritableWaveFileModel.cpp Thu May 16 12:54:58 2019 +0100 +++ b/data/model/WritableWaveFileModel.cpp Fri May 17 10:02:43 2019 +0100 @@ -221,15 +221,6 @@ return (m_model && m_model->isOK()); } -bool -WritableWaveFileModel::isReady(int *completion) const -{ - int c = getCompletion(); - if (completion) *completion = c; - if (!isOK()) return false; - return (c == 100); -} - void WritableWaveFileModel::setWriteProportion(int proportion) { diff -r ab4fd193262b -r 978c143c767f data/model/WritableWaveFileModel.h --- a/data/model/WritableWaveFileModel.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/WritableWaveFileModel.h Fri May 17 10:02:43 2019 +0100 @@ -139,7 +139,6 @@ int getWriteProportion() const; bool isOK() const override; - bool isReady(int *) const override; /** * Return the generation completion percentage of this model. This @@ -147,7 +146,7 @@ * -- it just contains varying amounts of data depending on how * much has been written. */ - virtual int getCompletion() const { return 100; } + int getCompletion() const override { return 100; } const ZoomConstraint *getZoomConstraint() const override { static PowerOfSqrtTwoZoomConstraint zc; diff -r ab4fd193262b -r 978c143c767f data/model/test/MockWaveModel.h --- a/data/model/test/MockWaveModel.h Thu May 16 12:54:58 2019 +0100 +++ b/data/model/test/MockWaveModel.h Fri May 17 10:02:43 2019 +0100 @@ -51,6 +51,7 @@ sv_frame_t getEndFrame() const override { return m_data[0].size(); } sv_samplerate_t getSampleRate() const override { return 44100; } bool isOK() const override { return true; } + int getCompletion() const override { return 100; } QString getTypeName() const override { return tr("Mock Wave"); } diff -r ab4fd193262b -r 978c143c767f data/model/test/TestSparseModels.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/data/model/test/TestSparseModels.h Fri May 17 10:02:43 2019 +0100 @@ -0,0 +1,305 @@ +/* -*- 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_SPARSE_MODELS_H +#define TEST_SPARSE_MODELS_H + +#include "../SparseOneDimensionalModel.h" +#include "../NoteModel.h" +#include "../TextModel.h" +#include "../PathModel.h" +#include "../ImageModel.h" + +#include +#include + +#include + +using namespace std; + +// NB model & dataset IDs in the export tests are incremental, +// depending on how many have been exported in previous tests - so +// when adding or removing tests we may occasionally need to update +// the IDs in other ones + +class TestSparseModels : public QObject +{ + Q_OBJECT + +private slots: + void s1d_empty() { + SparseOneDimensionalModel m(100, 10, false); + QCOMPARE(m.isEmpty(), true); + QCOMPARE(m.getEventCount(), 0); + QCOMPARE(m.getAllEvents().size(), size_t(0)); + QCOMPARE(m.getStartFrame(), 0); + QCOMPARE(m.getEndFrame(), 0); + QCOMPARE(m.getSampleRate(), 100.0); + QCOMPARE(m.getResolution(), 10); + QCOMPARE(m.isSparse(), true); + + Event p(10); + m.add(p); + m.remove(p); + QCOMPARE(m.isEmpty(), true); + QCOMPARE(m.getEventCount(), 0); + QCOMPARE(m.getAllEvents().size(), size_t(0)); + QCOMPARE(m.getStartFrame(), 0); + QCOMPARE(m.getEndFrame(), 0); + } + + void s1d_extents() { + SparseOneDimensionalModel m(100, 10, false); + Event p1(20); + m.add(p1); + QCOMPARE(m.isEmpty(), false); + QCOMPARE(m.getEventCount(), 1); + Event p2(50); + m.add(p2); + QCOMPARE(m.isEmpty(), false); + QCOMPARE(m.getEventCount(), 2); + QCOMPARE(m.getAllEvents().size(), size_t(2)); + QCOMPARE(*m.getAllEvents().begin(), p1); + QCOMPARE(*m.getAllEvents().rbegin(), p2); + QCOMPARE(m.getStartFrame(), 20); + QCOMPARE(m.getEndFrame(), 60); + QCOMPARE(m.containsEvent(p1), true); + m.remove(p1); + QCOMPARE(m.getEventCount(), 1); + QCOMPARE(m.getAllEvents().size(), size_t(1)); + QCOMPARE(*m.getAllEvents().begin(), p2); + QCOMPARE(m.getStartFrame(), 50); + QCOMPARE(m.getEndFrame(), 60); + QCOMPARE(m.containsEvent(p1), false); + } + + void s1d_sample() { + SparseOneDimensionalModel m(100, 10, false); + Event p1(20), p2(20), p3(50); + m.add(p1); + m.add(p2); + m.add(p3); + QCOMPARE(m.getAllEvents().size(), size_t(3)); + QCOMPARE(*m.getAllEvents().begin(), p1); + QCOMPARE(*m.getAllEvents().rbegin(), p3); + + // The EventSeries that is used internally is tested more + // thoroughly in its own test suite. This is just a check + auto pp = m.getEventsWithin(20, 10); + QCOMPARE(pp.size(), size_t(2)); + QCOMPARE(*pp.begin(), p1); + QCOMPARE(*pp.rbegin(), p2); + + pp = m.getEventsWithin(40, 10); + QCOMPARE(pp.size(), size_t(0)); + + pp = m.getEventsStartingAt(50); + QCOMPARE(pp.size(), size_t(1)); + QCOMPARE(*pp.begin(), p3); + } + + void s1d_xml() { + SparseOneDimensionalModel m(100, 10, false); + m.setObjectName("This \"&\" that"); + Event p1(20); + Event p2(20, "Label &'\">"); + Event p3(50, 12.4f, 16, ""); // value + duration should not be saved + m.add(p1); + m.add(p2); + m.add(p3); + QString xml; + QTextStream str(&xml, QIODevice::WriteOnly); + m.toXml(str); + str.flush(); + QString expected = + "\n" + "\n" + " \n" + " \n" + " \n" + "\n"; + expected.replace("\'", "\""); + if (xml != expected) { + cerr << "Obtained xml:\n" << xml + << "\nExpected:\n" << expected << std::endl; + } + QCOMPARE(xml, expected); + } + + void note_extents() { + NoteModel m(100, 10, false); + Event p1(20, 123.4f, 40, 0.8f, "note 1"); + m.add(p1); + QCOMPARE(m.isEmpty(), false); + QCOMPARE(m.getEventCount(), 1); + Event p2(50, 124.3f, 30, 0.9f, "note 2"); + m.add(p2); + QCOMPARE(m.isEmpty(), false); + QCOMPARE(m.getEventCount(), 2); + QCOMPARE(m.getAllEvents().size(), size_t(2)); + QCOMPARE(*m.getAllEvents().begin(), p1); + QCOMPARE(*m.getAllEvents().rbegin(), p2); + QCOMPARE(m.getStartFrame(), 20); + QCOMPARE(m.getEndFrame(), 80); + QCOMPARE(m.containsEvent(p1), true); + QCOMPARE(m.getValueMinimum(), 123.4f); + QCOMPARE(m.getValueMaximum(), 124.3f); + m.remove(p1); + QCOMPARE(m.getEventCount(), 1); + QCOMPARE(m.getAllEvents().size(), size_t(1)); + QCOMPARE(*m.getAllEvents().begin(), p2); + QCOMPARE(m.getStartFrame(), 50); + QCOMPARE(m.getEndFrame(), 80); + QCOMPARE(m.containsEvent(p1), false); + } + + void note_sample() { + NoteModel m(100, 10, false); + Event p1(20, 123.4f, 10, 0.8f, "note 1"); + Event p2(20, 124.3f, 20, 0.9f, "note 2"); + Event p3(50, 126.3f, 30, 0.9f, "note 3"); + m.add(p1); + m.add(p2); + m.add(p3); + + QCOMPARE(m.getAllEvents().size(), size_t(3)); + QCOMPARE(*m.getAllEvents().begin(), p1); + QCOMPARE(*m.getAllEvents().rbegin(), p3); + + auto pp = m.getEventsSpanning(20, 10); + QCOMPARE(pp.size(), size_t(2)); + QCOMPARE(*pp.begin(), p1); + QCOMPARE(*pp.rbegin(), p2); + + pp = m.getEventsSpanning(30, 20); + QCOMPARE(pp.size(), size_t(1)); + QCOMPARE(*pp.begin(), p2); + + pp = m.getEventsSpanning(40, 10); + QCOMPARE(pp.size(), size_t(0)); + + pp = m.getEventsCovering(50); + QCOMPARE(pp.size(), size_t(1)); + QCOMPARE(*pp.begin(), p3); + } + + void note_xml() { + NoteModel m(100, 10, false); + Event p1(20, 123.4f, 20, 0.8f, "note 1"); + Event p2(20, 124.3f, 10, 0.9f, "note 2"); + Event p3(50, 126.3f, 30, 0.9f, "note 3"); + m.setScaleUnits("Hz"); + m.add(p1); + m.add(p2); + m.add(p3); + QString xml; + QTextStream str(&xml, QIODevice::WriteOnly); + m.toXml(str); + str.flush(); + + QString expected = + "\n" + "\n" + " \n" + " \n" + " \n" + "\n"; + expected.replace("\'", "\""); + if (xml != expected) { + cerr << "Obtained xml:\n" << xml + << "\nExpected:\n" << expected << std::endl; + } + QCOMPARE(xml, expected); + } + + void text_xml() { + TextModel m(100, 10, false); + Event p1(20, 1.0f, "text 1"); + Event p2(20, 0.0f, "text 2"); + Event p3(50, 0.3f, "text 3"); + m.add(p1); + m.add(p2.withLevel(0.8f)); + m.add(p3); + QString xml; + QTextStream str(&xml, QIODevice::WriteOnly); + m.toXml(str); + str.flush(); + + QString expected = + "\n" + "\n" + " \n" + " \n" + " \n" + "\n"; + expected.replace("\'", "\""); + if (xml != expected) { + cerr << "Obtained xml:\n" << xml + << "\nExpected:\n" << expected << std::endl; + } + QCOMPARE(xml, expected); + } + + void path_xml() { + PathModel m(100, 10, false); + PathPoint p1(20, 30); + PathPoint p2(40, 60); + PathPoint p3(50, 49); + m.add(p1); + m.add(p2); + m.add(p3); + QString xml; + QTextStream str(&xml, QIODevice::WriteOnly); + m.toXml(str); + str.flush(); + + QString expected = + "\n" + "\n" + " \n" + " \n" + " \n" + "\n"; + expected.replace("\'", "\""); + if (xml != expected) { + cerr << "Obtained xml:\n" << xml + << "\nExpected:\n" << expected << std::endl; + } + QCOMPARE(xml, expected); + } + + void image_xml() { + ImageModel m(100, 10, false); + Event p1(20, 30, 40, "a label"); // value + duration should not be saved + m.add(p1.withURI("/path/to/thing.png").withLevel(0.8f)); + QString xml; + QTextStream str(&xml, QIODevice::WriteOnly); + m.toXml(str); + str.flush(); + + QString expected = + "\n" + "\n" + " \n" + "\n"; + expected.replace("\'", "\""); + if (xml != expected) { + cerr << "Obtained xml:\n" << xml + << "\nExpected:\n" << expected << std::endl; + } + QCOMPARE(xml, expected); + } +}; + +#endif diff -r ab4fd193262b -r 978c143c767f data/model/test/files.pri --- a/data/model/test/files.pri Thu May 16 12:54:58 2019 +0100 +++ b/data/model/test/files.pri Fri May 17 10:02:43 2019 +0100 @@ -2,6 +2,7 @@ Compares.h \ MockWaveModel.h \ TestFFTModel.h \ + TestSparseModels.h \ TestWaveformOversampler.h \ TestZoomConstraints.h diff -r ab4fd193262b -r 978c143c767f data/model/test/svcore-data-model-test.cpp --- a/data/model/test/svcore-data-model-test.cpp Thu May 16 12:54:58 2019 +0100 +++ b/data/model/test/svcore-data-model-test.cpp Fri May 17 10:02:43 2019 +0100 @@ -14,6 +14,7 @@ #include "TestFFTModel.h" #include "TestZoomConstraints.h" #include "TestWaveformOversampler.h" +#include "TestSparseModels.h" #include "system/Init.h" @@ -31,7 +32,7 @@ QCoreApplication app(argc, argv); app.setOrganizationName("sonic-visualiser"); - app.setApplicationName("test-model"); + app.setApplicationName("test-svcore-data-model"); { TestFFTModel t; @@ -51,6 +52,12 @@ else ++bad; } + { + TestSparseModels t; + if (QTest::qExec(&t, argc, argv) == 0) ++good; + else ++bad; + } + if (bad > 0) { SVCERR << "\n********* " << bad << " test suite(s) failed!\n" << endl; return 1; diff -r ab4fd193262b -r 978c143c767f data/osc/OSCMessage.h --- a/data/osc/OSCMessage.h Thu May 16 12:54:58 2019 +0100 +++ b/data/osc/OSCMessage.h Fri May 17 10:02:43 2019 +0100 @@ -50,6 +50,18 @@ int getArgCount() const; const QVariant &getArg(int i) const; + // For debugging purposes, not for interchange + QString toString() const { + QString s = QString("[%1][%2] %3") + .arg(m_target) + .arg(m_targetData) + .arg(m_method); + for (auto a: m_args) { + s.push_back(" \"" + a.toString() + "\""); + } + return s; + } + private: int m_target; int m_targetData; diff -r ab4fd193262b -r 978c143c767f data/osc/OSCMessageCallback.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/data/osc/OSCMessageCallback.h Fri May 17 10:02:43 2019 +0100 @@ -0,0 +1,25 @@ +/* -*- 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_OSC_MESSAGE_CALLBACK_H +#define SV_OSC_MESSAGE_CALLBACK_H + +class OSCMessage; + +class OSCMessageCallback { +public: + virtual void handleOSCMessage(const OSCMessage &) = 0; +}; + +#endif diff -r ab4fd193262b -r 978c143c767f data/osc/OSCQueue.cpp --- a/data/osc/OSCQueue.cpp Thu May 16 12:54:58 2019 +0100 +++ b/data/osc/OSCQueue.cpp Fri May 17 10:02:43 2019 +0100 @@ -23,6 +23,7 @@ #include "base/Profiler.h" #include +#include #define OSC_MESSAGE_QUEUE_SIZE 1023 @@ -89,24 +90,37 @@ #endif -OSCQueue::OSCQueue() : +OSCQueue::OSCQueue(bool withNetworkPort) : #ifdef HAVE_LIBLO m_thread(nullptr), #endif + m_withPort(withNetworkPort), m_buffer(OSC_MESSAGE_QUEUE_SIZE) { Profiler profiler("OSCQueue::OSCQueue"); #ifdef HAVE_LIBLO - m_thread = lo_server_thread_new(nullptr, oscError); + if (m_withPort) { + m_thread = lo_server_thread_new(nullptr, oscError); - lo_server_thread_add_method(m_thread, nullptr, nullptr, - oscMessageHandler, this); + lo_server_thread_add_method(m_thread, nullptr, nullptr, + oscMessageHandler, this); - lo_server_thread_start(m_thread); + lo_server_thread_start(m_thread); - cout << "OSCQueue::OSCQueue: Base OSC URL is " - << lo_server_thread_get_url(m_thread) << endl; + SVDEBUG << "OSCQueue::OSCQueue: Started OSC thread, URL is " + << lo_server_thread_get_url(m_thread) << endl; + + cout << "OSCQueue::OSCQueue: Base OSC URL is " + << lo_server_thread_get_url(m_thread) << endl; + } +#else + if (m_withPort) { + SVDEBUG << "OSCQueue::OSCQueue: Note: OSC port support not " + << "compiled in; not opening port, falling back to " + << "internal-only queue" << endl; + m_withPort = false; + } #endif } @@ -126,11 +140,15 @@ bool OSCQueue::isOK() const { + if (!m_withPort) { + return true; + } else { #ifdef HAVE_LIBLO - return (m_thread != nullptr); + return (m_thread != nullptr); #else - return false; + return false; #endif + } } QString @@ -138,7 +156,9 @@ { QString url = ""; #ifdef HAVE_LIBLO - url = lo_server_thread_get_url(m_thread); + if (m_thread) { + url = lo_server_thread_get_url(m_thread); + } #endif return url; } @@ -155,6 +175,9 @@ OSCMessage *message = m_buffer.readOne(); OSCMessage rmessage = *message; delete message; + SVDEBUG << "OSCQueue::readMessage: In thread " + << QThread::currentThreadId() << ": message follows:\n" + << rmessage.toString() << endl; return rmessage; } @@ -180,8 +203,9 @@ OSCMessage *mp = new OSCMessage(message); m_buffer.write(&mp, 1); SVDEBUG << "OSCQueue::postMessage: Posted OSC message: target " - << message.getTarget() << ", target data " << message.getTargetData() - << ", method " << message.getMethod() << endl; + << message.getTarget() << ", target data " + << message.getTargetData() << ", method " + << message.getMethod() << endl; emit messagesAvailable(); } @@ -220,7 +244,8 @@ return false; } - SVDEBUG << "OSCQueue::parseOSCPath: good path \"" << path << "\"" << endl; + SVDEBUG << "OSCQueue::parseOSCPath: good path \"" << path + << "\"" << endl; return true; } diff -r ab4fd193262b -r 978c143c767f data/osc/OSCQueue.h --- a/data/osc/OSCQueue.h Thu May 16 12:54:58 2019 +0100 +++ b/data/osc/OSCQueue.h Fri May 17 10:02:43 2019 +0100 @@ -36,17 +36,20 @@ Q_OBJECT public: - OSCQueue(); + OSCQueue(bool withNetworkPort); virtual ~OSCQueue(); bool isOK() const; bool isEmpty() const { return getMessagesAvailable() == 0; } int getMessagesAvailable() const; + void postMessage(OSCMessage); OSCMessage readMessage(); QString getOSCURL() const; + bool hasPort() const { return m_withPort; } + signals: void messagesAvailable(); @@ -59,9 +62,9 @@ int, lo_message, void *); #endif - void postMessage(OSCMessage); bool parseOSCPath(QString path, int &target, int &targetData, QString &method); + bool m_withPort; RingBuffer m_buffer; }; diff -r ab4fd193262b -r 978c143c767f files.pri --- a/files.pri Thu May 16 12:54:58 2019 +0100 +++ b/files.pri Fri May 17 10:02:43 2019 +0100 @@ -7,11 +7,16 @@ base/ColumnOp.h \ base/Command.h \ base/Debug.h \ + base/Event.h \ + base/EventSeries.h \ base/Exceptions.h \ + base/Extents.h \ base/HelperExecPath.h \ base/HitCount.h \ base/LogRange.h \ base/MagnitudeRange.h \ + base/NoteData.h \ + base/NoteExportable.h \ base/Pitch.h \ base/Playable.h \ base/PlayParameterRepository.h \ @@ -75,25 +80,23 @@ data/model/Dense3DModelPeakCache.h \ data/model/DenseThreeDimensionalModel.h \ data/model/DenseTimeValueModel.h \ + data/model/DeferredNotifier.h \ data/model/EditableDenseThreeDimensionalModel.h \ + data/model/EventCommands.h \ data/model/FFTModel.h \ data/model/ImageModel.h \ - data/model/IntervalModel.h \ data/model/Labeller.h \ data/model/Model.h \ data/model/ModelDataTableModel.h \ data/model/NoteModel.h \ - data/model/FlexiNoteModel.h \ data/model/PathModel.h \ data/model/PowerOfSqrtTwoZoomConstraint.h \ data/model/PowerOfTwoZoomConstraint.h \ data/model/RangeSummarisableTimeValueModel.h \ data/model/RegionModel.h \ data/model/RelativelyFineZoomConstraint.h \ - data/model/SparseModel.h \ data/model/SparseOneDimensionalModel.h \ data/model/SparseTimeValueModel.h \ - data/model/SparseValueModel.h \ data/model/TabularModel.h \ data/model/TextModel.h \ data/model/WaveformOversampler.h \ @@ -101,6 +104,7 @@ data/model/ReadOnlyWaveFileModel.h \ data/model/WritableWaveFileModel.h \ data/osc/OSCMessage.h \ + data/osc/OSCMessageCallback.h \ data/osc/OSCQueue.h \ plugin/PluginScan.h \ plugin/DSSIPluginFactory.h \ @@ -149,6 +153,7 @@ base/ColumnOp.cpp \ base/Command.cpp \ base/Debug.cpp \ + base/EventSeries.cpp \ base/Exceptions.cpp \ base/HelperExecPath.cpp \ base/LogRange.cpp \ diff -r ab4fd193262b -r 978c143c767f rdf/RDFExporter.cpp --- a/rdf/RDFExporter.cpp Thu May 16 12:54:58 2019 +0100 +++ b/rdf/RDFExporter.cpp Fri May 17 10:02:43 2019 +0100 @@ -85,14 +85,13 @@ if (m) { f.hasTimestamp = true; f.hasDuration = true; - const RegionModel::PointList &pl(m->getPoints()); - for (RegionModel::PointList::const_iterator i = pl.begin(); - i != pl.end(); ++i) { - f.timestamp = RealTime::frame2RealTime(i->frame, sr).toVampRealTime(); - f.duration = RealTime::frame2RealTime(i->duration, sr).toVampRealTime(); + EventVector ee(m->getAllEvents()); + for (auto e: ee) { + f.timestamp = RealTime::frame2RealTime(e.getFrame(), sr).toVampRealTime(); + f.duration = RealTime::frame2RealTime(e.getDuration(), sr).toVampRealTime(); f.values.clear(); - f.values.push_back(i->value); - f.label = i->label.toStdString(); + f.values.push_back(e.getValue()); + f.label = e.getLabel().toStdString(); m_fw->write(trackId, transform, output, features, summaryType); } return; @@ -103,15 +102,14 @@ if (m) { f.hasTimestamp = true; f.hasDuration = true; - const NoteModel::PointList &pl(m->getPoints()); - for (NoteModel::PointList::const_iterator i = pl.begin(); - i != pl.end(); ++i) { - f.timestamp = RealTime::frame2RealTime(i->frame, sr).toVampRealTime(); - f.duration = RealTime::frame2RealTime(i->duration, sr).toVampRealTime(); + EventVector ee(m->getAllEvents()); + for (auto e: ee) { + f.timestamp = RealTime::frame2RealTime(e.getFrame(), sr).toVampRealTime(); + f.duration = RealTime::frame2RealTime(e.getDuration(), sr).toVampRealTime(); f.values.clear(); - f.values.push_back(i->value); - f.values.push_back(i->level); - f.label = i->label.toStdString(); + f.values.push_back(e.getValue()); + f.values.push_back(e.getLevel()); + f.label = e.getLabel().toStdString(); m_fw->write(trackId, transform, output, features, summaryType); } return; @@ -122,12 +120,11 @@ if (m) { f.hasTimestamp = true; f.hasDuration = false; - const SparseOneDimensionalModel::PointList &pl(m->getPoints()); - for (SparseOneDimensionalModel::PointList::const_iterator i = pl.begin(); - i != pl.end(); ++i) { - f.timestamp = RealTime::frame2RealTime(i->frame, sr).toVampRealTime(); + EventVector ee(m->getAllEvents()); + for (auto e: ee) { + f.timestamp = RealTime::frame2RealTime(e.getFrame(), sr).toVampRealTime(); f.values.clear(); - f.label = i->label.toStdString(); + f.label = e.getLabel().toStdString(); m_fw->write(trackId, transform, output, features, summaryType); } return; @@ -138,13 +135,12 @@ if (m) { f.hasTimestamp = true; f.hasDuration = false; - const SparseTimeValueModel::PointList &pl(m->getPoints()); - for (SparseTimeValueModel::PointList::const_iterator i = pl.begin(); - i != pl.end(); ++i) { - f.timestamp = RealTime::frame2RealTime(i->frame, sr).toVampRealTime(); + EventVector ee(m->getAllEvents()); + for (auto e: ee) { + f.timestamp = RealTime::frame2RealTime(e.getFrame(), sr).toVampRealTime(); f.values.clear(); - f.values.push_back(i->value); - f.label = i->label.toStdString(); + f.values.push_back(e.getValue()); + f.label = e.getLabel().toStdString(); m_fw->write(trackId, transform, output, features, summaryType); } return; @@ -155,14 +151,13 @@ if (m) { f.hasTimestamp = true; f.hasDuration = false; - const TextModel::PointList &pl(m->getPoints()); m_fw->setFixedEventTypeURI("af:Text"); - for (TextModel::PointList::const_iterator i = pl.begin(); - i != pl.end(); ++i) { - f.timestamp = RealTime::frame2RealTime(i->frame, sr).toVampRealTime(); + EventVector ee(m->getAllEvents()); + for (auto e: ee) { + f.timestamp = RealTime::frame2RealTime(e.getFrame(), sr).toVampRealTime(); f.values.clear(); - f.values.push_back(i->height); - f.label = i->label.toStdString(); + f.values.push_back(e.getValue()); + f.label = e.getLabel().toStdString(); m_fw->write(trackId, transform, output, features, summaryType); } return; diff -r ab4fd193262b -r 978c143c767f rdf/RDFImporter.cpp --- a/rdf/RDFImporter.cpp Thu May 16 12:54:58 2019 +0100 +++ b/rdf/RDFImporter.cpp Fri May 17 10:02:43 2019 +0100 @@ -347,8 +347,8 @@ for (int j = 0; j < values.size(); ++j) { float f = values[j].toFloat(); - SparseTimeValueModel::Point point(j * hopSize, f, ""); - m->addPoint(point); + Event e(j * hopSize, f, ""); + m->add(e); } getDenseModelTitle(m, feature, type); @@ -709,28 +709,27 @@ SparseOneDimensionalModel *sodm = dynamic_cast(model); if (sodm) { - SparseOneDimensionalModel::Point point(ftime, label); - sodm->addPoint(point); + Event point(ftime, label); + sodm->add(point); return; } TextModel *tm = dynamic_cast(model); if (tm) { - TextModel::Point point + Event e (ftime, values.empty() ? 0.5f : values[0] < 0.f ? 0.f : values[0] > 1.f ? 1.f : values[0], // I was young and feckless once too label); - tm->addPoint(point); + tm->add(e); return; } SparseTimeValueModel *stvm = dynamic_cast(model); if (stvm) { - SparseTimeValueModel::Point point - (ftime, values.empty() ? 0.f : values[0], label); - stvm->addPoint(point); + Event e(ftime, values.empty() ? 0.f : values[0], label); + stvm->add(e); return; } @@ -745,8 +744,8 @@ level = values[1]; } } - NoteModel::Point point(ftime, value, fduration, level, label); - nm->addPoint(point); + Event e(ftime, value, fduration, level, label); + nm->add(e); } else { float value = 0.f, duration = 1.f, level = 1.f; if (!values.empty()) { @@ -758,9 +757,9 @@ } } } - NoteModel::Point point(ftime, value, sv_frame_t(lrintf(duration)), - level, label); - nm->addPoint(point); + Event e(ftime, value, sv_frame_t(lrintf(duration)), + level, label); + nm->add(e); } return; } @@ -779,8 +778,8 @@ value = values[0]; } if (haveDuration) { - RegionModel::Point point(ftime, value, fduration, label); - rm->addPoint(point); + Event e(ftime, value, fduration, label); + rm->add(e); } else { // This won't actually happen -- we only create region models // if we do have duration -- but just for completeness @@ -791,9 +790,8 @@ duration = values[1]; } } - RegionModel::Point point(ftime, value, - sv_frame_t(lrintf(duration)), label); - rm->addPoint(point); + Event e(ftime, value, sv_frame_t(lrintf(duration)), label); + rm->add(e); } return; } diff -r ab4fd193262b -r 978c143c767f transform/FeatureExtractionModelTransformer.cpp --- a/transform/FeatureExtractionModelTransformer.cpp Thu May 16 12:54:58 2019 +0100 +++ b/transform/FeatureExtractionModelTransformer.cpp Fri May 17 10:02:43 2019 +0100 @@ -28,7 +28,6 @@ #include "data/model/EditableDenseThreeDimensionalModel.h" #include "data/model/DenseTimeValueModel.h" #include "data/model/NoteModel.h" -#include "data/model/FlexiNoteModel.h" #include "data/model/RegionModel.h" #include "data/model/FFTModel.h" #include "data/model/WaveFileModel.h" @@ -410,13 +409,8 @@ // count > 1). But we don't. QSettings settings; - settings.beginGroup("Transformer"); - bool flexi = settings.value("use-flexi-note-model", false).toBool(); - settings.endGroup(); - cerr << "flexi = " << flexi << endl; - - if (isNoteModel && !flexi) { + if (isNoteModel) { NoteModel *model; if (haveExtents) { @@ -429,19 +423,6 @@ model->setScaleUnits(m_descriptors[n]->unit.c_str()); out = model; - } else if (isNoteModel && flexi) { - - FlexiNoteModel *model; - if (haveExtents) { - model = new FlexiNoteModel - (modelRate, modelResolution, minValue, maxValue, false); - } else { - model = new FlexiNoteModel - (modelRate, modelResolution, false); - } - model->setScaleUnits(m_descriptors[n]->unit.c_str()); - out = model; - } else { RegionModel *model; @@ -996,8 +977,7 @@ getConformingOutput(n); if (!model) return; - model->addPoint(SparseOneDimensionalModel::Point - (frame, feature.label.c_str())); + model->add(Event(frame, feature.label.c_str())); } else if (isOutput(n)) { @@ -1023,11 +1003,10 @@ // << " for output " << n << " bin " << i << std::endl; } - targetModel->addPoint - (SparseTimeValueModel::Point(frame, value, label)); + targetModel->add(Event(frame, value, label)); } - } else if (isOutput(n) || isOutput(n) || isOutput(n)) { //GF: Added Note Model + } else if (isOutput(n) || isOutput(n)) { int index = 0; @@ -1045,24 +1024,7 @@ } } - if (isOutput(n)) { // GF: added for flexi note model - - float velocity = 100; - if ((int)feature.values.size() > index) { - velocity = feature.values[index++]; - } - if (velocity < 0) velocity = 127; - if (velocity > 127) velocity = 127; - - FlexiNoteModel *model = getConformingOutput(n); - if (!model) return; - model->addPoint(FlexiNoteModel::Point(frame, - value, // value is pitch - duration, - velocity / 127.f, - feature.label.c_str())); - // GF: end -- added for flexi note model - } else if (isOutput(n)) { + if (isOutput(n)) { float velocity = 100; if ((int)feature.values.size() > index) { @@ -1073,10 +1035,10 @@ NoteModel *model = getConformingOutput(n); if (!model) return; - model->addPoint(NoteModel::Point(frame, value, // value is pitch - duration, - velocity / 127.f, - feature.label.c_str())); + model->add(Event(frame, value, // value is pitch + duration, + velocity / 127.f, + feature.label.c_str())); } else { RegionModel *model = getConformingOutput(n); @@ -1093,17 +1055,17 @@ label = QString("[%1] %2").arg(i+1).arg(label); } - model->addPoint(RegionModel::Point(frame, - value, - duration, - label)); + model->add(Event(frame, + value, + duration, + label)); } } else { - model->addPoint(RegionModel::Point(frame, - value, - duration, - feature.label.c_str())); + model->add(Event(frame, + value, + duration, + feature.label.c_str())); } } @@ -1160,13 +1122,6 @@ if (model->isAbandoning()) abandon(); model->setCompletion(completion, true); - } else if (isOutput(n)) { - - FlexiNoteModel *model = getConformingOutput(n); - if (!model) return; - if (model->isAbandoning()) abandon(); - model->setCompletion(completion, true); - } else if (isOutput(n)) { RegionModel *model = getConformingOutput(n); diff -r ab4fd193262b -r 978c143c767f transform/ModelTransformerFactory.cpp --- a/transform/ModelTransformerFactory.cpp Thu May 16 12:54:58 2019 +0100 +++ b/transform/ModelTransformerFactory.cpp Fri May 17 10:02:43 2019 +0100 @@ -34,6 +34,7 @@ #include #include +#include using std::vector; @@ -59,6 +60,8 @@ sv_frame_t duration, UserConfigurator *configurator) { + QMutexLocker locker(&m_mutex); + ModelTransformer::Input input(nullptr); if (candidateInputModels.empty()) return input; @@ -213,6 +216,8 @@ { SVDEBUG << "ModelTransformerFactory::transformMultiple: Constructing transformer with input model " << input.getModel() << endl; + QMutexLocker locker(&m_mutex); + ModelTransformer *t = createTransformer(transforms, input); if (!t) return vector(); @@ -255,6 +260,8 @@ void ModelTransformerFactory::transformerFinished() { + QMutexLocker locker(&m_mutex); + QObject *s = sender(); ModelTransformer *transformer = dynamic_cast(s); @@ -299,17 +306,21 @@ { TransformerSet affected; - for (TransformerSet::iterator i = m_runningTransformers.begin(); - i != m_runningTransformers.end(); ++i) { + { + QMutexLocker locker(&m_mutex); + + for (TransformerSet::iterator i = m_runningTransformers.begin(); + i != m_runningTransformers.end(); ++i) { - ModelTransformer *t = *i; + ModelTransformer *t = *i; - if (t->getInputModel() == m) { - affected.insert(t); - } else { - vector mm = t->getOutputModels(); - for (int i = 0; i < (int)mm.size(); ++i) { - if (mm[i] == m) affected.insert(t); + if (t->getInputModel() == m) { + affected.insert(t); + } else { + vector mm = t->getOutputModels(); + for (int i = 0; i < (int)mm.size(); ++i) { + if (mm[i] == m) affected.insert(t); + } } } } @@ -327,3 +338,10 @@ } } +bool +ModelTransformerFactory::haveRunningTransformers() const +{ + QMutexLocker locker(&m_mutex); + + return (!m_runningTransformers.empty()); +} diff -r ab4fd193262b -r 978c143c767f transform/ModelTransformerFactory.h --- a/transform/ModelTransformerFactory.h Thu May 16 12:54:58 2019 +0100 +++ b/transform/ModelTransformerFactory.h Fri May 17 10:02:43 2019 +0100 @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -145,6 +146,8 @@ QString &message, AdditionalModelHandler *handler = 0); + bool haveRunningTransformers() const; + signals: void transformFailed(QString transformName, QString message); @@ -157,6 +160,8 @@ ModelTransformer *createTransformer(const Transforms &transforms, const ModelTransformer::Input &input); + mutable QMutex m_mutex; + typedef std::map TransformerConfigurationMap; TransformerConfigurationMap m_lastConfigurations; diff -r ab4fd193262b -r 978c143c767f transform/RealTimeEffectModelTransformer.cpp --- a/transform/RealTimeEffectModelTransformer.cpp Thu May 16 12:54:58 2019 +0100 +++ b/transform/RealTimeEffectModelTransformer.cpp Fri May 17 10:02:43 2019 +0100 @@ -266,8 +266,7 @@ if (pointFrame > latency) pointFrame -= latency; else pointFrame = 0; - stvm->addPoint(SparseTimeValueModel::Point - (pointFrame, value, "")); + stvm->add(Event(pointFrame, value, "")); } else if (wwfm) {