Chris@305: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
Chris@305: 
Chris@305: /*
Chris@305:     Sonic Visualiser
Chris@305:     An audio file viewer and annotation editor.
Chris@305:     Centre for Digital Music, Queen Mary, University of London.
Chris@305:     This file copyright 2006-2007 Chris Cannam and QMUL.
Chris@305:     
Chris@305:     This program is free software; you can redistribute it and/or
Chris@305:     modify it under the terms of the GNU General Public License as
Chris@305:     published by the Free Software Foundation; either version 2 of the
Chris@305:     License, or (at your option) any later version.  See the file
Chris@305:     COPYING included with this distribution for more information.
Chris@305: */
Chris@305: 
Chris@1581: #ifndef SV_LABELLER_H
Chris@1581: #define SV_LABELLER_H
Chris@305: 
Chris@1652: #include "base/Selection.h"
Chris@1652: #include "base/Event.h"
Chris@305: 
Chris@1652: #include "EventCommands.h"
Chris@305: 
Chris@305: #include <QObject>
Chris@305: 
Chris@305: #include <map>
Chris@305: #include <iostream>
Chris@305: 
Chris@305: class Labeller : public QObject
Chris@305: {
Chris@423:     Q_OBJECT
Chris@423: 
Chris@305: public:
Chris@305:     enum ValueType {
Chris@305:         ValueNone,
Chris@305:         ValueFromSimpleCounter,
Chris@305:         ValueFromCyclicalCounter,
Chris@305:         ValueFromTwoLevelCounter,
Chris@305:         ValueFromFrameNumber,
Chris@305:         ValueFromRealTime,
Chris@355:         ValueFromDurationFromPrevious,
Chris@355:         ValueFromDurationToNext,
Chris@355:         ValueFromTempoFromPrevious,
Chris@355:         ValueFromTempoToNext,
Chris@305:         ValueFromExistingNeighbour,
Chris@305:         ValueFromLabel
Chris@305:     };
Chris@305: 
Chris@305:     // uses:
Chris@305:     //
Chris@305:     // 1. when adding points to a time-value model, generate values
Chris@305:     // for those points based on their times or labels or a counter
Chris@305:     //
Chris@305:     // 2. when adding a single point to a time-instant model, generate
Chris@305:     // a label for it based on its time and that of the previous point
Chris@305:     // or a counter
Chris@305:     //
Chris@305:     // 3. when adding a single point to a time-instant model, generate
Chris@305:     // a label for the previous point based on its time and that of
Chris@305:     // the point just added (as tempo is based on time to the next
Chris@305:     // point, not the previous one)
Chris@305:     //
Chris@305:     // 4. re-label a set of points that have already been added to a
Chris@305:     // model
Chris@1652:     //
Chris@1652:     // 5. generate new labelled points in the gaps between other
Chris@1652:     // points (subdivide), or remove them (winnow)
Chris@305: 
Chris@355:     Labeller(ValueType type = ValueNone) :
Chris@305:         m_type(type),
Chris@305:         m_counter(1),
Chris@305:         m_counter2(1),
Chris@305:         m_cycle(4),
Chris@305:         m_dp(10),
Chris@305:         m_rate(0) { }
Chris@305: 
Chris@305:     Labeller(const Labeller &l) :
Chris@305:         QObject(),
Chris@305:         m_type(l.m_type),
Chris@380:         m_counter(l.m_counter),
Chris@380:         m_counter2(l.m_counter2),
Chris@305:         m_cycle(l.m_cycle),
Chris@305:         m_dp(l.m_dp),
Chris@305:         m_rate(l.m_rate) { }
Chris@305: 
Chris@305:     virtual ~Labeller() { }
Chris@305: 
Chris@305:     typedef std::map<ValueType, QString> TypeNameMap;
Chris@305:     TypeNameMap getTypeNames() const {
Chris@305:         TypeNameMap m;
Chris@355:         m[ValueNone]
Chris@355:             = tr("No numbering");
Chris@355:         m[ValueFromSimpleCounter]
Chris@355:             = tr("Simple counter");
Chris@355:         m[ValueFromCyclicalCounter]
Chris@355:             = tr("Cyclical counter");
Chris@355:         m[ValueFromTwoLevelCounter]
Chris@355:             = tr("Cyclical two-level counter (bar/beat)");
Chris@355:         m[ValueFromFrameNumber]
Chris@355:             = tr("Audio sample frame number");
Chris@355:         m[ValueFromRealTime]
Chris@355:             = tr("Time in seconds");
Chris@355:         m[ValueFromDurationToNext]
Chris@355:             = tr("Duration to the following item");
Chris@355:         m[ValueFromTempoToNext]
Chris@355:             = tr("Tempo (bpm) based on duration to following item");
Chris@355:         m[ValueFromDurationFromPrevious]
Chris@355:             = tr("Duration since the previous item");
Chris@355:         m[ValueFromTempoFromPrevious]
Chris@355:             = tr("Tempo (bpm) based on duration since previous item");
Chris@355:         m[ValueFromExistingNeighbour]
Chris@355:             = tr("Same as the nearest previous item");
Chris@355:         m[ValueFromLabel]
Chris@355:             = tr("Value extracted from the item's label (where possible)");
Chris@305:         return m;
Chris@305:     }
Chris@305: 
Chris@305:     ValueType getType() const { return m_type; }
Chris@305:     void setType(ValueType type) { m_type = type; }
Chris@305: 
Chris@305:     int getCounterValue() const { return m_counter; }
Chris@305:     void setCounterValue(int v) { m_counter = v; }
Chris@305: 
Chris@305:     int getSecondLevelCounterValue() const { return m_counter2; }
Chris@305:     void setSecondLevelCounterValue(int v) { m_counter2 = v; }
Chris@305: 
Chris@305:     int getCounterCycleSize() const { return m_cycle; }
Chris@305:     void setCounterCycleSize(int s) {
Chris@305:         m_cycle = s;
Chris@305:         m_dp = 1;
Chris@305:         while (s > 0) {
Chris@305:             s /= 10;
Chris@305:             m_dp *= 10;
Chris@305:         }
Chris@307:         if (m_counter > m_cycle) m_counter = 1;
Chris@305:     }
Chris@305: 
Chris@1046:     void setSampleRate(sv_samplerate_t rate) { m_rate = rate; }
Chris@305: 
Chris@830:     void resetCounters() {
Chris@830:         m_counter = 1;
Chris@830:         m_counter2 = 1;
Chris@830:         m_cycle = 4;
Chris@830:     }
Chris@830: 
Chris@305:     void incrementCounter() {
Chris@305:         m_counter++;
Chris@305:         if (m_type == ValueFromCyclicalCounter ||
Chris@305:             m_type == ValueFromTwoLevelCounter) {
Chris@305:             if (m_counter > m_cycle) {
Chris@305:                 m_counter = 1;
Chris@305:                 m_counter2++;
Chris@305:             }
Chris@305:         }
Chris@305:     }
Chris@305: 
Chris@1652:     enum Application {
Chris@1652:         AppliesToThisEvent,
Chris@1652:         AppliesToPreviousEvent
Chris@1652:     };
Chris@1652:     typedef std::pair<Application, Event> Relabelling;
Chris@1652:     typedef std::pair<Application, Event> Revaluing;
Chris@1652: 
Chris@1652:     /** 
Chris@1652:      * Return a labelled event based on the given event, previous
Chris@1652:      * event if supplied, and internal labeller state. The returned
Chris@1652:      * event replaces either the event provided or the previous event,
Chris@1652:      * depending on the Application value in the returned pair.
Chris@1652:      */
Chris@1652:     Relabelling
Chris@1652:     label(Event e, const Event *prev = nullptr) {
Chris@1652: 
Chris@1652:         QString label = e.getLabel();
Chris@1652: 
Chris@305:         if (m_type == ValueNone) {
Chris@1652:             label = "";
Chris@305:         } else if (m_type == ValueFromTwoLevelCounter) {
Chris@1652:             label = tr("%1.%2").arg(m_counter2).arg(m_counter);
Chris@305:             incrementCounter();
Chris@306:         } else if (m_type == ValueFromFrameNumber) {
Chris@306:             // avoid going through floating-point value
Chris@1652:             label = tr("%1").arg(e.getFrame());
Chris@305:         } else {
Chris@1652:             float value = getValueFor(e, prev);
Chris@1652:             label = QString("%1").arg(value);
Chris@1652:         }
Chris@1652: 
Chris@1652:         if (actingOnPrevEvent() && prev) {
Chris@1652:             return { AppliesToPreviousEvent, prev->withLabel(label) };
Chris@1652:         } else {
Chris@1652:             return { AppliesToThisEvent, e.withLabel(label) };
Chris@305:         }
Chris@305:     }
Chris@1652: 
Chris@1652:     /** 
Chris@1652:      * Return an event with a value following the labelling scheme,
Chris@1652:      * based on the given event, previous event if supplied, and
Chris@1652:      * internal labeller state. The returned event replaces either the
Chris@1652:      * event provided or the previous event, depending on the
Chris@1652:      * Application value in the returned pair.
Chris@1652:      */
Chris@1652:     Revaluing
Chris@1652:     revalue(Event e, const Event *prev = nullptr) {
Chris@1652: 
Chris@1652:         float value = e.getValue();
Chris@305:         
Chris@1652:         if (m_type == ValueFromExistingNeighbour) {
Chris@1652:             if (!prev) {
Chris@1652:                 std::cerr << "ERROR: Labeller::setValue: Previous point required but not provided" << std::endl;
Chris@1652:             } else {
Chris@1652:                 return { AppliesToThisEvent, e.withValue(prev->getValue()) };
Chris@1652:             }
Chris@1652:         } else {
Chris@1652:             value = getValueFor(e, prev);
Chris@1652:         }
Chris@1652: 
Chris@1652:         if (actingOnPrevEvent() && prev) {
Chris@1652:             return { AppliesToPreviousEvent, prev->withValue(value) };
Chris@1652:         } else {
Chris@1652:             return { AppliesToThisEvent, e.withValue(value) };
Chris@1652:         }
Chris@1652:     }
Chris@1652:     
Chris@1291:     /**
Chris@1652:      * Relabel all events in the given event vector that lie within
Chris@1652:      * the given multi-selection, according to the labelling
Chris@1652:      * properties of this labeller.  Return a command that has been
Chris@1742:      * executed but not yet added to the history.  The id must be that
Chris@1742:      * of a type that can be retrieved from the AnyById store and
Chris@1742:      * dynamic_cast to EventEditable.
Chris@1291:      */
Chris@1742:     Command *labelAll(int editableId,
Chris@1652:                       MultiSelection *ms,
Chris@1652:                       const EventVector &allEvents) {
Chris@305: 
Chris@1742:         auto command = new ChangeEventsCommand
Chris@1742:             (editableId, tr("Label Points"));
Chris@305: 
Chris@1652:         Event prev;
Chris@1652:         bool havePrev = false;
Chris@305: 
Chris@1652:         for (auto p: allEvents) {
Chris@305: 
Chris@305:             if (ms) {
Chris@1652:                 Selection s(ms->getContainingSelection(p.getFrame(), false));
Chris@1652:                 if (!s.contains(p.getFrame())) {
Chris@1652:                     prev = p;
Chris@1652:                     havePrev = true;
Chris@1293:                     continue;
Chris@305:                 }
Chris@305:             }
Chris@305: 
Chris@1652:             auto labelling = label(p, havePrev ? &prev : nullptr);
Chris@1652: 
Chris@1652:             if (labelling.first == AppliesToThisEvent) {
Chris@1652:                 command->remove(p);
Chris@305:             } else {
Chris@1652:                 command->remove(prev);
Chris@305:             }
Chris@305: 
Chris@1652:             command->add(labelling.second);
Chris@1652: 
Chris@1652:             prev = p;
Chris@1652:             havePrev = true;
Chris@305:         }
Chris@305: 
Chris@1291:         return command->finish();
Chris@1291:     }
Chris@1291: 
Chris@1291:     /**
Chris@1652:      * For each event in the given event vector (except the last), if
Chris@1652:      * that event lies within the given multi-selection, add n-1 new
Chris@1652:      * events at equally spaced intervals between it and the following
Chris@1652:      * event.  Return a command that has been executed but not yet
Chris@1742:      * added to the history.  The id must be that of a type that can
Chris@1742:      * be retrieved from the AnyById store and dynamic_cast to
Chris@1742:      * EventEditable.
Chris@1291:      */
Chris@1742:     Command *subdivide(int editableId,
Chris@1652:                        MultiSelection *ms,
Chris@1652:                        const EventVector &allEvents,
Chris@1652:                        int n) {
Chris@1291: 
Chris@1742:         auto command = new ChangeEventsCommand
Chris@1742:             (editableId, tr("Subdivide Points"));
Chris@1652: 
Chris@1652:         for (auto i = allEvents.begin(); i != allEvents.end(); ++i) {
Chris@1291: 
Chris@1291:             auto j = i;
Chris@1291:             // require a "next point" even if it's not in selection
Chris@1652:             if (++j == allEvents.end()) {
Chris@1291:                 break;
Chris@1291:             }
Chris@1291: 
Chris@1291:             if (ms) {
Chris@1652:                 Selection s(ms->getContainingSelection(i->getFrame(), false));
Chris@1652:                 if (!s.contains(i->getFrame())) {
Chris@1293:                     continue;
Chris@1291:                 }
Chris@1291:             }
Chris@1291: 
Chris@1652:             Event p(*i);
Chris@1652:             Event nextP(*j);
Chris@1291: 
Chris@1291:             // n is the number of subdivisions, so we add n-1 new
Chris@1291:             // points equally spaced between p and nextP
Chris@1291: 
Chris@1291:             for (int m = 1; m < n; ++m) {
Chris@1652:                 sv_frame_t f = p.getFrame() +
Chris@1652:                     (m * (nextP.getFrame() - p.getFrame())) / n;
Chris@1652:                 Event newPoint = p
Chris@1652:                     .withFrame(f)
Chris@1652:                     .withLabel(tr("%1.%2").arg(p.getLabel()).arg(m+1));
Chris@1652:                 command->add(newPoint);
Chris@1291:             }
Chris@1291:         }
Chris@1291: 
Chris@1291:         return command->finish();
Chris@305:     }
Chris@305: 
Chris@1292:     /**
Chris@1652:      * The opposite of subdivide. Given an event vector, a
Chris@1652:      * multi-selection, and a number n, remove all but every nth event
Chris@1652:      * from the vector within the extents of the multi-selection.
Chris@1292:      * Return a command that has been executed but not yet added to
Chris@1742:      * the history. The id must be that of a type that can be
Chris@1742:      * retrieved from the AnyById store and dynamic_cast to
Chris@1742:      * EventEditable.
Chris@1292:      */
Chris@1742:     Command *winnow(int editableId,
Chris@1652:                     MultiSelection *ms,
Chris@1652:                     const EventVector &allEvents,
Chris@1652:                     int n) {
Chris@1652: 
Chris@1742:         auto command = new ChangeEventsCommand
Chris@1742:             (editableId, tr("Winnow Points"));
Chris@1292: 
Chris@1292:         int counter = 0;
Chris@1292:         
Chris@1652:         for (auto p: allEvents) {
Chris@1292: 
Chris@1292:             if (ms) {
Chris@1652:                 Selection s(ms->getContainingSelection(p.getFrame(), false));
Chris@1652:                 if (!s.contains(p.getFrame())) {
Chris@1293:                     counter = 0;
Chris@1293:                     continue;
Chris@1292:                 }
Chris@1292:             }
Chris@1292: 
Chris@1292:             ++counter;
Chris@1292: 
Chris@1292:             if (counter == n+1) counter = 1;
Chris@1292:             if (counter == 1) {
Chris@1292:                 // this is an Nth instant, don't remove it
Chris@1292:                 continue;
Chris@1292:             }
Chris@1292:             
Chris@1652:             command->remove(p);
Chris@1292:         }
Chris@1292: 
Chris@1292:         return command->finish();
Chris@1292:     }
Chris@1292: 
Chris@355:     bool requiresPrevPoint() const {
Chris@355:         return (m_type == ValueFromDurationFromPrevious ||
Chris@355:                 m_type == ValueFromDurationToNext ||
Chris@355:                 m_type == ValueFromTempoFromPrevious ||
Chris@355:                 m_type == ValueFromDurationToNext);
Chris@355:     }
Chris@355: 
Chris@1652:     bool actingOnPrevEvent() const {
Chris@355:         return (m_type == ValueFromDurationToNext ||
Chris@355:                 m_type == ValueFromTempoToNext);
Chris@305:     }
Chris@305: 
Chris@305: protected:
Chris@1652:     float getValueFor(Event p, const Event *prev) {
Chris@1652:         
Chris@305:         float value = 0.f;
Chris@305: 
Chris@305:         switch (m_type) {
Chris@305: 
Chris@305:         case ValueNone:
Chris@305:             value = 0;
Chris@305:             break;
Chris@305: 
Chris@305:         case ValueFromSimpleCounter:
Chris@305:         case ValueFromCyclicalCounter:
Chris@1046:             value = float(m_counter);
Chris@305:             incrementCounter();
Chris@305:             break;
Chris@305: 
Chris@305:         case ValueFromTwoLevelCounter:
Chris@1046:             value = float(m_counter2 + double(m_counter) / double(m_dp));
Chris@305:             incrementCounter();
Chris@305:             break;
Chris@305: 
Chris@305:         case ValueFromFrameNumber:
Chris@1652:             value = float(p.getFrame());
Chris@305:             break;
Chris@305:             
Chris@305:         case ValueFromRealTime: 
Chris@1046:             if (m_rate == 0.0) {
Chris@305:                 std::cerr << "ERROR: Labeller::getValueFor: Real-time conversion required, but no sample rate set" << std::endl;
Chris@305:             } else {
Chris@1652:                 value = float(double(p.getFrame()) / m_rate);
Chris@305:             }
Chris@305:             break;
Chris@305: 
Chris@355:         case ValueFromDurationToNext:
Chris@355:         case ValueFromTempoToNext:
Chris@355:         case ValueFromDurationFromPrevious:
Chris@355:         case ValueFromTempoFromPrevious:
Chris@1046:             if (m_rate == 0.0) {
Chris@305:                 std::cerr << "ERROR: Labeller::getValueFor: Real-time conversion required, but no sample rate set" << std::endl;
Chris@1652:             } else if (!prev) {
Chris@305:                 std::cerr << "ERROR: Labeller::getValueFor: Time difference required, but only one point provided" << std::endl;
Chris@305:             } else {
Chris@1652:                 sv_frame_t f0 = prev->getFrame(), f1 = p.getFrame();
Chris@355:                 if (m_type == ValueFromDurationToNext ||
Chris@355:                     m_type == ValueFromDurationFromPrevious) {
Chris@1046:                     value = float(double(f1 - f0) / m_rate);
Chris@305:                 } else {
Chris@305:                     if (f1 > f0) {
Chris@1046:                         value = float((60.0 * m_rate) / double(f1 - f0));
Chris@305:                     }
Chris@305:                 }
Chris@305:             }
Chris@305:             break;
Chris@305: 
Chris@305:         case ValueFromExistingNeighbour:
Chris@305:             // need to deal with this in the calling function, as this
Chris@305:             // function must handle points that don't have values to
Chris@305:             // read from
Chris@305:             break;
Chris@305: 
Chris@305:         case ValueFromLabel:
Chris@1652:             if (p.getLabel() != "") {
Chris@305:                 // more forgiving than QString::toFloat()
Chris@1652:                 value = float(atof(p.getLabel().toLocal8Bit()));
Chris@305:             } else {
Chris@305:                 value = 0.f;
Chris@305:             }
Chris@305:             break;
Chris@305:         }
Chris@305: 
Chris@305:         return value;
Chris@305:     }
Chris@305: 
Chris@305:     ValueType m_type;
Chris@305:     int m_counter;
Chris@305:     int m_counter2;
Chris@305:     int m_cycle;
Chris@305:     int m_dp;
Chris@1046:     sv_samplerate_t m_rate;
Chris@305: };
Chris@305: 
Chris@305: #endif