547 lines
21 KiB
C++
547 lines
21 KiB
C++
//=============================================================================
|
|
// MuseScore
|
|
// Music Composition & Notation
|
|
//
|
|
// Copyright (C) 2009-2011 Werner Schweer
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License version 2
|
|
// as published by the Free Software Foundation and appearing in
|
|
// the file LICENCE.GPL
|
|
//=============================================================================
|
|
|
|
/**
|
|
\file
|
|
Implementation of class ChangeMap.
|
|
*/
|
|
|
|
#include "changeMap.h"
|
|
|
|
namespace Ms {
|
|
|
|
//---------------------------------------------------------
|
|
// interpolateVelocity
|
|
/// the maths looks complex, but is just a series of graph transformations.
|
|
/// You can see these graphically at: https://www.desmos.com/calculator/kk89ficmjk
|
|
//---------------------------------------------------------
|
|
|
|
int ChangeMap::interpolate(Fraction& eventTick, ChangeEvent& event, Fraction& tick)
|
|
{
|
|
Q_ASSERT(event.type == ChangeEventType::RAMP);
|
|
|
|
// Prevent zero-division error
|
|
if (event.cachedStartVal == event.cachedEndVal || event.length.isZero()) {
|
|
return event.cachedStartVal;
|
|
}
|
|
|
|
// Ticks to change expression over
|
|
int exprTicks = event.length.ticks();
|
|
int exprDiff = event.cachedEndVal - event.cachedStartVal;
|
|
|
|
std::function<int(int)> valueFunction;
|
|
switch (event.method) {
|
|
case ChangeMethod::EXPONENTIAL:
|
|
// Due to the nth-root, exponential functions do not flip with negative values, and cause errors,
|
|
// so treat it as a piecewise function.
|
|
if (exprDiff > 0) {
|
|
valueFunction = [&](int ct) { return int(
|
|
pow(
|
|
pow((exprDiff + 1), 1.0 / double(exprTicks)), // the exprTicks root of d+1
|
|
double(ct) // to the power of the current tick (exponential)
|
|
) - 1
|
|
); };
|
|
}
|
|
else {
|
|
valueFunction = [&](int ct) { return -int(
|
|
pow(
|
|
pow((-exprDiff + 1), 1.0 / double(exprTicks)), // the exprTicks root of 1-d
|
|
double(ct) // again to the power of ct
|
|
) + 1
|
|
); };
|
|
}
|
|
break;
|
|
// Uses sin x transformed, which _does_ flip with negative numbers
|
|
case ChangeMethod::EASE_IN_OUT:
|
|
valueFunction = [&](int ct) { return int(
|
|
(double(exprDiff) / 2.0) * (
|
|
sin(
|
|
double(ct) * (
|
|
double(M_PI / double(exprTicks))
|
|
) - double(M_PI / 2.0)
|
|
) + 1
|
|
)
|
|
); };
|
|
break;
|
|
case ChangeMethod::EASE_IN:
|
|
valueFunction = [&](int ct) { return int(
|
|
double(exprDiff) * (
|
|
sin(
|
|
double(ct - double(exprTicks)) * (
|
|
double(M_PI / double(2 * exprTicks))
|
|
)
|
|
) + 1
|
|
)
|
|
); };
|
|
break;
|
|
case ChangeMethod::EASE_OUT:
|
|
valueFunction = [&](int ct) { return int(
|
|
double(exprDiff) * sin(
|
|
double(ct) * (
|
|
double(M_PI / double(2 * exprTicks))
|
|
)
|
|
)
|
|
); };
|
|
break;
|
|
case ChangeMethod::NORMAL:
|
|
default:
|
|
valueFunction = [&](int ct) { return int(
|
|
double(exprDiff) * (double(ct) / double(exprTicks))
|
|
); };
|
|
break;
|
|
}
|
|
|
|
return event.cachedStartVal + valueFunction(tick.ticks() - eventTick.ticks());
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// val
|
|
/// return value at tick position. Do not confuse with
|
|
/// `value`, which is a method of QMultiMap.
|
|
//---------------------------------------------------------
|
|
|
|
int ChangeMap::val(Fraction tick)
|
|
{
|
|
if (!cleanedUp)
|
|
cleanup();
|
|
|
|
auto eventIter = upperBound(tick);
|
|
if (eventIter == begin()) {
|
|
return DEFAULT_VALUE;
|
|
}
|
|
|
|
eventIter--;
|
|
bool foundRamp = false;
|
|
ChangeEvent& rampFound = eventIter.value(); // only used to init
|
|
Fraction rampFoundStartTick = eventIter.key();
|
|
for (auto& event : values(rampFoundStartTick)) {
|
|
if (event.type == ChangeEventType::RAMP) {
|
|
foundRamp = true;
|
|
rampFound = event;
|
|
}
|
|
}
|
|
|
|
if (!foundRamp) {
|
|
// Last event must be a fix, since there are max two events at one tick
|
|
return eventIter.value().value;
|
|
}
|
|
|
|
if (tick >= (rampFoundStartTick + rampFound.length)) {
|
|
return rampFound.cachedEndVal;
|
|
}
|
|
else {
|
|
// Do some maths!
|
|
return interpolate(rampFoundStartTick, rampFound, tick); // NOTE:JT check should rampFound be eventIter.value()
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// addFixed
|
|
//---------------------------------------------------------
|
|
|
|
void ChangeMap::addFixed(Fraction tick, int value)
|
|
{
|
|
insert(tick, ChangeEvent(value));
|
|
cleanedUp = false;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// addRamp
|
|
/// A `change` of 0 means that the change in velocity should be calculated from the next fixed
|
|
/// velocity event.
|
|
//---------------------------------------------------------
|
|
|
|
void ChangeMap::addRamp(Fraction stick, Fraction etick, int change, ChangeMethod method, ChangeDirection direction)
|
|
{
|
|
change = abs(change);
|
|
change *= (direction == ChangeDirection::INCREASING) ? 1 : -1;
|
|
insert(stick, ChangeEvent(stick, etick, change, method, direction));
|
|
cleanedUp = false;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// cleanupStage0
|
|
/// put the ramps in size order if they start at the same point
|
|
//---------------------------------------------------------
|
|
|
|
void ChangeMap::cleanupStage0()
|
|
{
|
|
for (auto& tick : uniqueKeys()) {
|
|
// rampEvents will contain all the ramps at this tick
|
|
std::vector<ChangeEvent> rampEvents;
|
|
for (auto& event : values(tick)) {
|
|
if (event.type == ChangeEventType::FIX)
|
|
continue;
|
|
rampEvents.push_back(event);
|
|
}
|
|
|
|
if (int(rampEvents.size()) > 1) {
|
|
// Sort rampEvents so that the longest ramps come first -
|
|
// this is important for when we remove ramps/fixes enclosed wihtin other
|
|
// ramps during stage 1.
|
|
std::sort(rampEvents.begin(), rampEvents.end(), ChangeMap::compareRampEvents);
|
|
for (auto& event : rampEvents) {
|
|
insert(tick, event);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// cleanupStage1
|
|
/// remove any ramps or fixes that are completely enclosed within other ramps
|
|
//---------------------------------------------------------
|
|
|
|
void ChangeMap::cleanupStage1()
|
|
{
|
|
Fraction currentRampStart = Fraction(-1, 1); // start point of ramp we're in
|
|
Fraction currentRampEnd = Fraction(-1, 1); // end point of ramp we're in
|
|
Fraction lastFix = Fraction(-1, 1); // the position of the last fix event
|
|
bool inRamp = false; // whether we're in a ramp or not
|
|
|
|
// Keep a record of the endpoints
|
|
EndPointsVector endPoints;
|
|
std::vector<bool> startsInRamp;
|
|
|
|
auto i = begin();
|
|
while (i != end()) {
|
|
Fraction tick = i.key();
|
|
ChangeEvent& event = i.value();
|
|
Fraction etick = tick + event.length;
|
|
|
|
// Reset if we've left the ramp we were in
|
|
if (currentRampEnd < tick)
|
|
inRamp = false;
|
|
|
|
if (event.type == ChangeEventType::RAMP) {
|
|
if (inRamp) {
|
|
if (etick <= currentRampEnd) {
|
|
// delete, this event is enveloped
|
|
i = erase(i);
|
|
// don't add to the end points
|
|
continue;
|
|
}
|
|
else {
|
|
currentRampStart = tick;
|
|
currentRampEnd = etick;
|
|
startsInRamp.push_back(true);
|
|
}
|
|
}
|
|
else {
|
|
currentRampStart = tick;
|
|
currentRampEnd = etick;
|
|
inRamp = true;
|
|
startsInRamp.push_back(false);
|
|
}
|
|
|
|
endPoints.push_back(std::make_pair(tick, etick));
|
|
}
|
|
else if (event.type == ChangeEventType::FIX) {
|
|
if (inRamp) {
|
|
if (tick != currentRampStart && tick != currentRampEnd && lastFix != tick) {
|
|
// delete, this event is enveloped or at the same point as another fix
|
|
i = erase(i);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
lastFix = tick;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
cleanupStage2(startsInRamp, endPoints);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// cleanupStage2
|
|
/// readjust lengths of any colliding ramps
|
|
//---------------------------------------------------------
|
|
|
|
void ChangeMap::cleanupStage2(std::vector<bool>& startsInRamp, EndPointsVector& endPoints)
|
|
{
|
|
// moveTo stores the events that need to be moved to a Fraction position
|
|
std::map<Fraction, ChangeEvent> moveTo;
|
|
auto i = begin();
|
|
int j = -1;
|
|
while (i != end()) {
|
|
Fraction tick = i.key();
|
|
ChangeEvent& event = i.value();
|
|
if (event.type != ChangeEventType::RAMP) {
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
j++;
|
|
if (!startsInRamp[j]) {
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
// Take a copy of the event and remove it
|
|
Fraction newTick = endPoints[j-1].second;
|
|
event.length -= (newTick - tick);
|
|
moveTo[newTick] = event;
|
|
i = erase(i);
|
|
}
|
|
|
|
// Re-insert the events that we need to move in their new positions
|
|
for (auto k = moveTo.begin(); k != moveTo.end(); k++) {
|
|
insert(k->first, k->second);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// cleanupStage3
|
|
/// cache start and end values for each ramp
|
|
//---------------------------------------------------------
|
|
|
|
void ChangeMap::cleanupStage3()
|
|
{
|
|
for (auto i = begin(); i != end(); i++) {
|
|
Fraction tick = i.key();
|
|
auto& event = i.value();
|
|
if (event.type != ChangeEventType::RAMP)
|
|
continue;
|
|
|
|
// Phase 1: cache a start value for the ramp
|
|
// Try and get a fix at the tick of this ramp
|
|
bool foundFix = false;
|
|
for (auto& currentChangeEvent : values(tick)) {
|
|
if (currentChangeEvent.type == ChangeEventType::FIX) {
|
|
event.cachedStartVal = currentChangeEvent.value;
|
|
foundFix = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If there isn't a fix, use from the last event:
|
|
// - the cached end value if it's a ramp
|
|
// - the value if it's a fix
|
|
if (!foundFix) {
|
|
if (i != begin()) {
|
|
auto prevChangeEventIter = i;
|
|
prevChangeEventIter--;
|
|
|
|
// Look for a ramp first
|
|
bool foundRamp = false;
|
|
for (auto& prevChangeEvent : values(prevChangeEventIter.key())) {
|
|
if (prevChangeEvent.type == ChangeEventType::RAMP) {
|
|
event.cachedStartVal = prevChangeEvent.cachedEndVal;
|
|
foundRamp = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!foundRamp) {
|
|
// prevChangeEventIter must point to a fix in this case
|
|
event.cachedStartVal = prevChangeEventIter.value().value;
|
|
}
|
|
}
|
|
else {
|
|
event.cachedStartVal = DEFAULT_VALUE;
|
|
}
|
|
}
|
|
|
|
// Phase 2: cache an end value for the ramp
|
|
// If there's no set velocity change:
|
|
if (event.value == 0) {
|
|
auto nextChangeEventIter = i;
|
|
nextChangeEventIter++;
|
|
// There's a chance that the next event is a fix at the same tick as the
|
|
// start of the current ramp. If so, get the next event, which is assured
|
|
// to be a different (larger) tick
|
|
if (nextChangeEventIter != end() && nextChangeEventIter.key() == tick)
|
|
nextChangeEventIter++;
|
|
|
|
// If this is the last event, there is no change
|
|
if (nextChangeEventIter == end()) {
|
|
event.cachedEndVal = event.cachedStartVal;
|
|
}
|
|
else {
|
|
// Search for a fixed event at the next event point
|
|
bool foundFix2 = false;
|
|
for (auto& nextChangeEvent : values(nextChangeEventIter.key())) {
|
|
if (nextChangeEvent.type == ChangeEventType::FIX) {
|
|
event.cachedEndVal = nextChangeEvent.value;
|
|
foundFix2 = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// We haven't found a fix, so there must be a ramp. What does the user want?
|
|
// A good guess would to be to interpolate, but that might get complex, so just ignore
|
|
// this ramp and set the ending to be the same as the start.
|
|
// TODO: implementing some form of smart interpolation would be nice.
|
|
if (!foundFix2) {
|
|
event.cachedEndVal = event.cachedStartVal;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
event.cachedEndVal = event.cachedStartVal + event.value;
|
|
}
|
|
|
|
// And finally... if something's wrong, make it not wrong
|
|
if ((event.cachedStartVal > event.cachedEndVal && event.direction == ChangeDirection::INCREASING) ||
|
|
(event.cachedStartVal < event.cachedEndVal && event.direction == ChangeDirection::DECREASING)) {
|
|
event.cachedEndVal = event.cachedStartVal;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// cleanup
|
|
//---------------------------------------------------------
|
|
|
|
void ChangeMap::cleanup()
|
|
{
|
|
if (cleanedUp)
|
|
return;
|
|
|
|
// qDebug() << "Before cleanup:";
|
|
// dump();
|
|
|
|
cleanupStage0();
|
|
cleanupStage1();
|
|
cleanupStage3();
|
|
cleanedUp = true;
|
|
|
|
// qDebug() << "After cleanup:";
|
|
// dump();
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// changesInRange
|
|
/// returns a list of changes in a range, and their start and end points
|
|
//---------------------------------------------------------
|
|
|
|
std::vector<std::pair<Fraction, Fraction>> ChangeMap::changesInRange(Fraction stick, Fraction etick)
|
|
{
|
|
if (!cleanedUp)
|
|
cleanup();
|
|
|
|
std::vector<std::pair<Fraction, Fraction>> tempChanges;
|
|
|
|
// Force a new event on every noteon, in case the velocity has changed
|
|
tempChanges.push_back(std::make_pair(stick, stick));
|
|
for (auto iter = lowerBound(stick); iter != end(); iter++) {
|
|
Fraction tick = iter.key();
|
|
if (tick > etick)
|
|
break;
|
|
|
|
auto& event = iter.value();
|
|
if (event.type == ChangeEventType::FIX)
|
|
tempChanges.push_back(std::make_pair(tick, tick));
|
|
else if (event.type == ChangeEventType::RAMP) {
|
|
Fraction eventEtick = tick + event.length;
|
|
Fraction useEtick = eventEtick > etick ? etick : eventEtick;
|
|
tempChanges.push_back(std::make_pair(tick, useEtick));
|
|
}
|
|
}
|
|
|
|
// And also go back one and try to find ramp coming into this range
|
|
auto iter = lowerBound(stick);
|
|
if (iter != begin()) {
|
|
iter--;
|
|
auto& event = iter.value();
|
|
if (event.type == ChangeEventType::RAMP) {
|
|
Fraction eventEtick = iter.key() + event.length;
|
|
if (eventEtick > stick) {
|
|
tempChanges.push_back(std::make_pair(stick, eventEtick));
|
|
}
|
|
}
|
|
}
|
|
return tempChanges;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// changeMethodTable
|
|
//---------------------------------------------------------
|
|
|
|
const std::vector<ChangeMap::ChangeMethodItem> ChangeMap::changeMethodTable {
|
|
{ ChangeMethod::NORMAL, "normal" },
|
|
{ ChangeMethod::EASE_IN, "ease-in" },
|
|
{ ChangeMethod::EASE_OUT, "ease-out" },
|
|
{ ChangeMethod::EASE_IN_OUT, "ease-in-out" },
|
|
{ ChangeMethod::EXPONENTIAL, "exponential" },
|
|
};
|
|
|
|
//---------------------------------------------------------
|
|
// changeMethodToName
|
|
//---------------------------------------------------------
|
|
|
|
QString ChangeMap::changeMethodToName(ChangeMethod method)
|
|
{
|
|
for (auto i : ChangeMap::changeMethodTable) {
|
|
if (i.method == method)
|
|
return i.name;
|
|
}
|
|
qFatal("Unrecognised change method!");
|
|
return "none"; // silence a compiler warning
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// nameToChangeMethod
|
|
//---------------------------------------------------------
|
|
|
|
ChangeMethod ChangeMap::nameToChangeMethod(QString name)
|
|
{
|
|
for (auto i : ChangeMap::changeMethodTable) {
|
|
if (i.name == name)
|
|
return i.method;
|
|
}
|
|
return ChangeMethod::NORMAL; // default
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// dump
|
|
//---------------------------------------------------------
|
|
|
|
void ChangeMap::dump()
|
|
{
|
|
qDebug("\n\n=== ChangeMap: dump ===");
|
|
for (auto i = begin(); i != end(); i++) {
|
|
Fraction tick = i.key();
|
|
auto& event = i.value();
|
|
if (event.type == ChangeEventType::FIX) {
|
|
qDebug().nospace() << "===" << tick.ticks() << " : FIX " << event.value;
|
|
}
|
|
else if (event.type == ChangeEventType::RAMP) {
|
|
qDebug().nospace() << "===" << tick.ticks() << " to " << (tick + event.length).ticks() << " : RAMP diff " << event.value << " " << ChangeMap::changeMethodToName(event.method) << " (" << event.cachedStartVal << ", " << event.cachedEndVal << ")";
|
|
}
|
|
else if (event.type == ChangeEventType::INVALID) {
|
|
qDebug().nospace() << "===" << tick.ticks() << " : INVALID value" << event.value;
|
|
}
|
|
}
|
|
qDebug("=== ChangeMap: dump end ===\n\n");
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// operator==
|
|
//---------------------------------------------------------
|
|
|
|
bool ChangeEvent::operator==(const ChangeEvent& event) const
|
|
{
|
|
return (
|
|
value == event.value &&
|
|
type == event.type &&
|
|
length == event.length &&
|
|
method == event.method &&
|
|
direction == event.direction
|
|
);
|
|
}
|
|
|
|
}
|
|
|