view data/model/Labeller.h @ 1652:08bed13d3a26 single-point

Update Labeller to new event API + fewer magical side-effects
author Chris Cannam
date Tue, 19 Mar 2019 13:05:56 +0000
parents ad5f892c0c4d
children 9d82b164f264
line wrap: on
line source
/* -*- 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-2007 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_LABELLER_H
#define SV_LABELLER_H

#include "base/Selection.h"
#include "base/Event.h"

#include "EventCommands.h"

#include <QObject>

#include <map>
#include <iostream>

class Labeller : public QObject
{
    Q_OBJECT

public:
    enum ValueType {
        ValueNone,
        ValueFromSimpleCounter,
        ValueFromCyclicalCounter,
        ValueFromTwoLevelCounter,
        ValueFromFrameNumber,
        ValueFromRealTime,
        ValueFromDurationFromPrevious,
        ValueFromDurationToNext,
        ValueFromTempoFromPrevious,
        ValueFromTempoToNext,
        ValueFromExistingNeighbour,
        ValueFromLabel
    };

    // uses:
    //
    // 1. when adding points to a time-value model, generate values
    // for those points based on their times or labels or a counter
    //
    // 2. when adding a single point to a time-instant model, generate
    // a label for it based on its time and that of the previous point
    // or a counter
    //
    // 3. when adding a single point to a time-instant model, generate
    // a label for the previous point based on its time and that of
    // the point just added (as tempo is based on time to the next
    // point, not the previous one)
    //
    // 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),
        m_counter(1),
        m_counter2(1),
        m_cycle(4),
        m_dp(10),
        m_rate(0) { }

    Labeller(const Labeller &l) :
        QObject(),
        m_type(l.m_type),
        m_counter(l.m_counter),
        m_counter2(l.m_counter2),
        m_cycle(l.m_cycle),
        m_dp(l.m_dp),
        m_rate(l.m_rate) { }

    virtual ~Labeller() { }

    typedef std::map<ValueType, QString> TypeNameMap;
    TypeNameMap getTypeNames() const {
        TypeNameMap m;
        m[ValueNone]
            = tr("No numbering");
        m[ValueFromSimpleCounter]
            = tr("Simple counter");
        m[ValueFromCyclicalCounter]
            = tr("Cyclical counter");
        m[ValueFromTwoLevelCounter]
            = tr("Cyclical two-level counter (bar/beat)");
        m[ValueFromFrameNumber]
            = tr("Audio sample frame number");
        m[ValueFromRealTime]
            = tr("Time in seconds");
        m[ValueFromDurationToNext]
            = tr("Duration to the following item");
        m[ValueFromTempoToNext]
            = tr("Tempo (bpm) based on duration to following item");
        m[ValueFromDurationFromPrevious]
            = tr("Duration since the previous item");
        m[ValueFromTempoFromPrevious]
            = tr("Tempo (bpm) based on duration since previous item");
        m[ValueFromExistingNeighbour]
            = tr("Same as the nearest previous item");
        m[ValueFromLabel]
            = tr("Value extracted from the item's label (where possible)");
        return m;
    }

    ValueType getType() const { return m_type; }
    void setType(ValueType type) { m_type = type; }

    int getCounterValue() const { return m_counter; }
    void setCounterValue(int v) { m_counter = v; }

    int getSecondLevelCounterValue() const { return m_counter2; }
    void setSecondLevelCounterValue(int v) { m_counter2 = v; }

    int getCounterCycleSize() const { return m_cycle; }
    void setCounterCycleSize(int s) {
        m_cycle = s;
        m_dp = 1;
        while (s > 0) {
            s /= 10;
            m_dp *= 10;
        }
        if (m_counter > m_cycle) m_counter = 1;
    }

    void setSampleRate(sv_samplerate_t rate) { m_rate = rate; }

    void resetCounters() {
        m_counter = 1;
        m_counter2 = 1;
        m_cycle = 4;
    }

    void incrementCounter() {
        m_counter++;
        if (m_type == ValueFromCyclicalCounter ||
            m_type == ValueFromTwoLevelCounter) {
            if (m_counter > m_cycle) {
                m_counter = 1;
                m_counter2++;
            }
        }
    }

    enum Application {
        AppliesToThisEvent,
        AppliesToPreviousEvent
    };
    typedef std::pair<Application, Event> Relabelling;
    typedef std::pair<Application, Event> 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) {
            label = "";
        } else if (m_type == ValueFromTwoLevelCounter) {
            label = tr("%1.%2").arg(m_counter2).arg(m_counter);
            incrementCounter();
        } else if (m_type == ValueFromFrameNumber) {
            // avoid going through floating-point value
            label = tr("%1").arg(e.getFrame());
        } else {
            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 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.
     */
    Command *labelAll(EventEditable *editable,
                      MultiSelection *ms,
                      const EventVector &allEvents) {

        ChangeEventsCommand *command = new ChangeEventsCommand
            (editable, tr("Label Points"));

        Event prev;
        bool havePrev = false;

        for (auto p: allEvents) {

            if (ms) {
                Selection s(ms->getContainingSelection(p.getFrame(), false));
                if (!s.contains(p.getFrame())) {
                    prev = p;
                    havePrev = true;
                    continue;
                }
            }

            auto labelling = label(p, havePrev ? &prev : nullptr);

            if (labelling.first == AppliesToThisEvent) {
                command->remove(p);
            } else {
                command->remove(prev);
            }

            command->add(labelling.second);

            prev = p;
            havePrev = true;
        }

        return command->finish();
    }

    /**
     * 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.
     */
    Command *subdivide(EventEditable *editable,
                       MultiSelection *ms,
                       const EventVector &allEvents,
                       int n) {

        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 == allEvents.end()) {
                break;
            }

            if (ms) {
                Selection s(ms->getContainingSelection(i->getFrame(), false));
                if (!s.contains(i->getFrame())) {
                    continue;
                }
            }

            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.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);
            }
        }

        return command->finish();
    }

    /**
     * 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.
     */
    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: allEvents) {

            if (ms) {
                Selection s(ms->getContainingSelection(p.getFrame(), false));
                if (!s.contains(p.getFrame())) {
                    counter = 0;
                    continue;
                }
            }

            ++counter;

            if (counter == n+1) counter = 1;
            if (counter == 1) {
                // this is an Nth instant, don't remove it
                continue;
            }
            
            command->remove(p);
        }

        return command->finish();
    }

    bool requiresPrevPoint() const {
        return (m_type == ValueFromDurationFromPrevious ||
                m_type == ValueFromDurationToNext ||
                m_type == ValueFromTempoFromPrevious ||
                m_type == ValueFromDurationToNext);
    }

    bool actingOnPrevEvent() const {
        return (m_type == ValueFromDurationToNext ||
                m_type == ValueFromTempoToNext);
    }

protected:
    float getValueFor(Event p, const Event *prev) {
        
        float value = 0.f;

        switch (m_type) {

        case ValueNone:
            value = 0;
            break;

        case ValueFromSimpleCounter:
        case ValueFromCyclicalCounter:
            value = float(m_counter);
            incrementCounter();
            break;

        case ValueFromTwoLevelCounter:
            value = float(m_counter2 + double(m_counter) / double(m_dp));
            incrementCounter();
            break;

        case ValueFromFrameNumber:
            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(p.getFrame()) / m_rate);
            }
            break;

        case ValueFromDurationToNext:
        case ValueFromTempoToNext:
        case ValueFromDurationFromPrevious:
        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 (!prev) {
                std::cerr << "ERROR: Labeller::getValueFor: Time difference required, but only one point provided" << std::endl;
            } else {
                sv_frame_t f0 = prev->getFrame(), f1 = p.getFrame();
                if (m_type == ValueFromDurationToNext ||
                    m_type == ValueFromDurationFromPrevious) {
                    value = float(double(f1 - f0) / m_rate);
                } else {
                    if (f1 > f0) {
                        value = float((60.0 * m_rate) / double(f1 - f0));
                    }
                }
            }
            break;

        case ValueFromExistingNeighbour:
            // need to deal with this in the calling function, as this
            // function must handle points that don't have values to
            // read from
            break;

        case ValueFromLabel:
            if (p.getLabel() != "") {
                // more forgiving than QString::toFloat()
                value = float(atof(p.getLabel().toLocal8Bit()));
            } else {
                value = 0.f;
            }
            break;
        }

        return value;
    }

    ValueType m_type;
    int m_counter;
    int m_counter2;
    int m_cycle;
    int m_dp;
    sv_samplerate_t m_rate;
};

#endif