Mercurial > hg > svcore
diff data/model/NoteModel.h @ 1713:978c143c767f
Merge from branch single-point
author | Chris Cannam |
---|---|
date | Fri, 17 May 2019 10:02:43 +0100 |
parents | 73077ec5aed6 |
children | 78fe29adfd16 |
line wrap: on
line diff
--- 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 <QMutex> +#include <QMutexLocker> -/** - * 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<point frame=\"%2\" value=\"%3\" duration=\"%4\" level=\"%5\" label=\"%6\" %7/>\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<Note>, 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<Note>(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<Note>(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<Note>::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<bool(Event)> 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<Note>::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<Note>::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