2280 lines
93 KiB
C++
2280 lines
93 KiB
C++
#include "importmidi_tuplet.h"
|
|
#include "importmidi_chord.h"
|
|
#include "importmidi_meter.h"
|
|
#include "importmidi_quant.h"
|
|
#include "importmidi_inner.h"
|
|
#include "libmscore/mscore.h"
|
|
#include "libmscore/staff.h"
|
|
#include "libmscore/score.h"
|
|
#include "libmscore/fraction.h"
|
|
#include "libmscore/duration.h"
|
|
#include "libmscore/measure.h"
|
|
#include "libmscore/tuplet.h"
|
|
#include "preferences.h"
|
|
|
|
|
|
#include <set>
|
|
|
|
|
|
namespace Ms {
|
|
namespace MidiTuplet {
|
|
|
|
const std::map<int, TupletLimits>& tupletsLimits()
|
|
{
|
|
const static std::map<int, TupletLimits> values = {
|
|
{2, {{2, 3}, 1, 2, 2}},
|
|
{3, {{3, 2}, 1, 3, 3}},
|
|
{4, {{4, 3}, 3, 4, 3}},
|
|
{5, {{5, 4}, 3, 4, 4}},
|
|
{7, {{7, 8}, 4, 6, 5}},
|
|
{9, {{9, 8}, 6, 7, 7}}
|
|
};
|
|
return values;
|
|
}
|
|
|
|
const TupletLimits& tupletLimits(int tupletNumber)
|
|
{
|
|
auto it = tupletsLimits().find(tupletNumber);
|
|
|
|
Q_ASSERT_X(it != tupletsLimits().end(), "MidiTuplet::tupletValue", "Unknown tuplet");
|
|
|
|
return it->second;
|
|
}
|
|
|
|
int voiceLimit()
|
|
{
|
|
const auto operations = preferences.midiImportOperations.currentTrackOperations();
|
|
return (operations.useMultipleVoices) ? VOICES : 1;
|
|
}
|
|
|
|
int tupletVoiceLimit()
|
|
{
|
|
const auto operations = preferences.midiImportOperations.currentTrackOperations();
|
|
// for multiple voices: one voice is reserved for non-tuplet chords
|
|
return (operations.useMultipleVoices) ? VOICES - 1 : 1;
|
|
}
|
|
|
|
bool isTupletAllowed(const TupletInfo &tupletInfo)
|
|
{
|
|
{
|
|
// special check for duplets and triplets
|
|
const std::vector<int> nums = {2, 3};
|
|
// for duplet: if note first and single - only 1/2*tupletLen duration is allowed
|
|
// for triplet: if note first and single - only 1/3*tupletLen duration is allowed
|
|
for (int num: nums) {
|
|
if (tupletInfo.tupletNumber == num
|
|
&& tupletInfo.chords.size() == 1
|
|
&& tupletInfo.firstChordIndex == 0) {
|
|
const auto &chordEventIt = tupletInfo.chords.begin()->second;
|
|
for (const auto ¬e: chordEventIt->second.notes) {
|
|
if ((note.offTime - chordEventIt->first
|
|
- tupletInfo.len / num).absValue() > tupletInfo.regularQuant)
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// for all tuplets
|
|
const int minAllowedNoteCount = tupletLimits(tupletInfo.tupletNumber).minNoteCount;
|
|
if ((int)tupletInfo.chords.size() < minAllowedNoteCount)
|
|
return false;
|
|
// allow duplets and quadruplets with the zero error == regular error
|
|
if (tupletInfo.tupletNumber == 2 || tupletInfo.tupletNumber == 4) {
|
|
if (tupletInfo.tupletSumError > tupletInfo.regularSumError)
|
|
return false;
|
|
else if (tupletInfo.tupletSumError == tupletInfo.regularSumError
|
|
&& tupletInfo.tupletSumError > ReducedFraction(0, 1))
|
|
return false;
|
|
}
|
|
else {
|
|
if (tupletInfo.tupletSumError >= tupletInfo.regularSumError)
|
|
return false;
|
|
}
|
|
// only notes with len >= (half tuplet note len) allowed
|
|
const auto tupletNoteLen = tupletInfo.len / tupletInfo.tupletNumber;
|
|
for (const auto &tupletChord: tupletInfo.chords) {
|
|
for (const auto ¬e: tupletChord.second->second.notes) {
|
|
if (note.offTime - tupletChord.first >= tupletNoteLen / 2)
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::vector<int> findTupletNumbers(const ReducedFraction &divLen,
|
|
const ReducedFraction &barFraction)
|
|
{
|
|
const auto operations = preferences.midiImportOperations.currentTrackOperations();
|
|
std::vector<int> tupletNumbers;
|
|
|
|
if (Meter::isCompound(barFraction) && divLen == Meter::beatLength(barFraction)) {
|
|
if (operations.tuplets.duplets)
|
|
tupletNumbers.push_back(2);
|
|
if (operations.tuplets.quadruplets)
|
|
tupletNumbers.push_back(4);
|
|
}
|
|
else {
|
|
if (operations.tuplets.triplets)
|
|
tupletNumbers.push_back(3);
|
|
if (operations.tuplets.quintuplets)
|
|
tupletNumbers.push_back(5);
|
|
if (operations.tuplets.septuplets)
|
|
tupletNumbers.push_back(7);
|
|
if (operations.tuplets.nonuplets)
|
|
tupletNumbers.push_back(9);
|
|
}
|
|
|
|
return tupletNumbers;
|
|
}
|
|
|
|
ReducedFraction findQuantizationError(const ReducedFraction &value,
|
|
const ReducedFraction &raster)
|
|
{
|
|
const auto quantizedValue = Quantize::quantizeValue(value, raster);
|
|
return (value - quantizedValue).absValue();
|
|
}
|
|
|
|
// return: <chordIter, minChordError>
|
|
|
|
std::pair<std::multimap<ReducedFraction, MidiChord>::iterator, ReducedFraction>
|
|
findBestChordForTupletNote(const ReducedFraction &tupletNotePos,
|
|
const ReducedFraction &quantValue,
|
|
const std::multimap<ReducedFraction, MidiChord>::iterator &startChordIt,
|
|
const std::multimap<ReducedFraction, MidiChord>::iterator &endChordIt)
|
|
{
|
|
// choose the best chord, if any, for this tuplet note
|
|
std::pair<std::multimap<ReducedFraction, MidiChord>::iterator, ReducedFraction> bestChord;
|
|
bestChord.first = endChordIt;
|
|
// check chords - whether they can be in tuplet without large error
|
|
bool firstLoop = true;
|
|
for (auto chordIt = startChordIt; chordIt != endChordIt; ++chordIt) {
|
|
auto tupletError = (chordIt->first - tupletNotePos).absValue();
|
|
if (tupletError > quantValue)
|
|
continue;
|
|
if (firstLoop) {
|
|
bestChord.first = chordIt;
|
|
bestChord.second = tupletError;
|
|
firstLoop = false;
|
|
continue;
|
|
}
|
|
if (tupletError < bestChord.second) {
|
|
bestChord.first = chordIt;
|
|
bestChord.second = tupletError;
|
|
}
|
|
}
|
|
return bestChord;
|
|
}
|
|
|
|
// find tuplets over which duration lies
|
|
|
|
std::vector<TupletData>
|
|
findTupletsInBarForDuration(int voice,
|
|
const ReducedFraction &barStartTick,
|
|
const ReducedFraction &durationOnTime,
|
|
const ReducedFraction &durationLen,
|
|
const std::multimap<ReducedFraction, TupletData> &tupletEvents)
|
|
{
|
|
std::vector<TupletData> tupletsData;
|
|
if (tupletEvents.empty())
|
|
return tupletsData;
|
|
auto tupletIt = tupletEvents.lower_bound(barStartTick);
|
|
|
|
while (tupletIt != tupletEvents.end()
|
|
&& tupletIt->first < durationOnTime + durationLen) {
|
|
if (tupletIt->second.voice == voice
|
|
&& durationOnTime < tupletIt->first + tupletIt->second.len) {
|
|
// if tuplet and duration intersect each other
|
|
auto tupletData = tupletIt->second;
|
|
// convert tuplet onTime to local bar ticks
|
|
tupletData.onTime -= barStartTick;
|
|
tupletsData.push_back(tupletData);
|
|
}
|
|
++tupletIt;
|
|
}
|
|
return tupletsData;
|
|
}
|
|
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData>::const_iterator
|
|
findTupletForTimeRange(int voice,
|
|
const ReducedFraction &onTime,
|
|
const ReducedFraction &len,
|
|
const std::multimap<ReducedFraction, TupletData> &tupletEvents)
|
|
{
|
|
Q_ASSERT_X(len >= ReducedFraction(0, 1),
|
|
"MidiTuplet::findTupletForTimeRange", "Negative length of time range");
|
|
|
|
if (tupletEvents.empty())
|
|
return tupletEvents.end();
|
|
|
|
auto it = tupletEvents.upper_bound(onTime + len);
|
|
if (it != tupletEvents.begin())
|
|
--it;
|
|
while (true) {
|
|
const auto &tupletData = it->second;
|
|
const auto &tupletOffTime = tupletData.onTime + tupletData.len;
|
|
if (tupletData.voice == voice
|
|
&& onTime >= tupletData.onTime
|
|
&& ((len > ReducedFraction(0, 1)
|
|
? onTime + len <= tupletOffTime
|
|
: onTime < tupletOffTime)
|
|
)) {
|
|
return it;
|
|
}
|
|
if (it == tupletEvents.begin())
|
|
break;
|
|
--it;
|
|
}
|
|
return tupletEvents.end();
|
|
}
|
|
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData>::const_iterator
|
|
findTupletContainsTime(int voice,
|
|
const ReducedFraction &time,
|
|
const std::multimap<ReducedFraction, TupletData> &tupletEvents)
|
|
{
|
|
return findTupletForTimeRange(voice, time, ReducedFraction(0, 1), tupletEvents);
|
|
}
|
|
|
|
// find sum length of gaps between successive chords
|
|
// less is better
|
|
|
|
ReducedFraction findSumLengthOfRests(const TupletInfo &tupletInfo,
|
|
const ReducedFraction &startBarTick)
|
|
{
|
|
auto beg = tupletInfo.onTime;
|
|
const auto tupletEndTime = tupletInfo.onTime + tupletInfo.len;
|
|
const auto tupletNoteLen = tupletInfo.len / tupletInfo.tupletNumber;
|
|
ReducedFraction sumLen = {0, 1};
|
|
|
|
for (const auto &chord: tupletInfo.chords) {
|
|
const auto staccatoIt = tupletInfo.staccatoChords.find(chord.first);
|
|
const MidiChord &midiChord = chord.second->second;
|
|
const auto &chordOnTime = (chord.second->first < startBarTick)
|
|
? startBarTick
|
|
: startBarTick + Quantize::quantizeValue(chord.second->first - startBarTick,
|
|
tupletInfo.tupletQuant);
|
|
if (beg < chordOnTime)
|
|
sumLen += (chordOnTime - beg);
|
|
ReducedFraction maxOffTime(0, 1);
|
|
for (int i = 0; i != midiChord.notes.size(); ++i) {
|
|
auto noteOffTime = midiChord.notes[i].offTime;
|
|
if (staccatoIt != tupletInfo.staccatoChords.end() && i == staccatoIt->second)
|
|
noteOffTime = chordOnTime + tupletNoteLen;
|
|
if (noteOffTime > maxOffTime)
|
|
maxOffTime = noteOffTime;
|
|
}
|
|
beg = startBarTick + Quantize::quantizeValue(maxOffTime - startBarTick,
|
|
tupletInfo.tupletQuant);
|
|
if (beg >= tupletEndTime)
|
|
break;
|
|
}
|
|
if (beg < tupletEndTime)
|
|
sumLen += (tupletEndTime - beg);
|
|
return sumLen;
|
|
}
|
|
|
|
TupletInfo findTupletApproximation(
|
|
const ReducedFraction &tupletLen,
|
|
int tupletNumber,
|
|
const ReducedFraction ®ularRaster,
|
|
const ReducedFraction &startTupletTime,
|
|
const std::multimap<ReducedFraction, MidiChord>::iterator &startChordIt,
|
|
const std::multimap<ReducedFraction, MidiChord>::iterator &endChordIt)
|
|
{
|
|
TupletInfo tupletInfo;
|
|
tupletInfo.tupletNumber = tupletNumber;
|
|
tupletInfo.onTime = startTupletTime;
|
|
tupletInfo.len = tupletLen;
|
|
tupletInfo.tupletQuant = tupletLen / tupletNumber;
|
|
while (tupletInfo.tupletQuant / 2 >= regularRaster)
|
|
tupletInfo.tupletQuant /= 2;
|
|
tupletInfo.regularQuant = regularRaster;
|
|
|
|
auto startTupletChordIt = startChordIt;
|
|
for (int k = 0; k != tupletNumber; ++k) {
|
|
auto tupletNotePos = startTupletTime + tupletLen / tupletNumber * k;
|
|
// choose the best chord, if any, for this tuplet note
|
|
auto bestChord = findBestChordForTupletNote(tupletNotePos, regularRaster,
|
|
startTupletChordIt, endChordIt);
|
|
if (bestChord.first == endChordIt)
|
|
continue; // no chord fits to this tuplet note position
|
|
// chord can be in tuplet
|
|
if (tupletInfo.chords.empty())
|
|
tupletInfo.firstChordIndex = k;
|
|
const auto &chordOnTime = bestChord.first->first;
|
|
tupletInfo.chords.insert({chordOnTime, bestChord.first});
|
|
tupletInfo.tupletSumError += bestChord.second;
|
|
// for next tuplet note we start from the next chord
|
|
// because chord for the next tuplet note cannot be earlier
|
|
startTupletChordIt = bestChord.first;
|
|
++startTupletChordIt;
|
|
// find chord quant error for a regular grid
|
|
auto regularError = findQuantizationError(bestChord.first->first, regularRaster);
|
|
tupletInfo.regularSumError += regularError;
|
|
}
|
|
|
|
return tupletInfo;
|
|
}
|
|
|
|
bool isMoreTupletVoicesAllowed(int voicesInUse, int availableVoices)
|
|
{
|
|
return !(voicesInUse >= availableVoices || voicesInUse >= tupletVoiceLimit());
|
|
}
|
|
|
|
std::pair<ReducedFraction, ReducedFraction>
|
|
tupletInterval(const TupletInfo &tuplet)
|
|
{
|
|
ReducedFraction tupletEnd = tuplet.onTime + tuplet.len;
|
|
|
|
for (const auto &chord: tuplet.chords) {
|
|
auto offTime = MChord::maxNoteOffTime(chord.second->second.notes);
|
|
// quantization with regular raster (not tuplet raster)
|
|
// so we don't have to measure time values from bar start
|
|
offTime = Quantize::quantizeValue(offTime, tuplet.regularQuant);
|
|
if (offTime > tupletEnd)
|
|
tupletEnd = offTime;
|
|
}
|
|
return std::make_pair(tuplet.onTime, tupletEnd);
|
|
}
|
|
|
|
bool haveIntersection(const std::pair<ReducedFraction, ReducedFraction> &interval1,
|
|
const std::pair<ReducedFraction, ReducedFraction> &interval2)
|
|
{
|
|
return interval1.second > interval2.first && interval1.first < interval2.second;
|
|
}
|
|
|
|
bool haveIntersection(const std::pair<ReducedFraction, ReducedFraction> &interval,
|
|
const std::vector<std::pair<ReducedFraction, ReducedFraction>> &intervals)
|
|
{
|
|
for (const auto &i: intervals) {
|
|
if (haveIntersection(i, interval))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
class TupletErrorResult
|
|
{
|
|
public:
|
|
TupletErrorResult(double t = 0.0,
|
|
double relPlaces = 0.0,
|
|
const ReducedFraction &r = ReducedFraction(0, 1),
|
|
size_t vc = 0,
|
|
size_t tc = 0)
|
|
: tupletAverageError(t)
|
|
, relativeUsedChordPlaces(relPlaces)
|
|
, sumLengthOfRests(r)
|
|
, voiceCount(vc)
|
|
, tupletCount(tc)
|
|
{}
|
|
|
|
bool isInitialized() const { return tupletCount != 0; }
|
|
|
|
bool operator<(const TupletErrorResult &er) const
|
|
{
|
|
double value = div(tupletAverageError, er.tupletAverageError)
|
|
- div(relativeUsedChordPlaces, er.relativeUsedChordPlaces)
|
|
+ div(sumLengthOfRests.numerator() * 1.0 / sumLengthOfRests.denominator(),
|
|
er.sumLengthOfRests.numerator() * 1.0 / er.sumLengthOfRests.denominator());
|
|
if (value == 0) {
|
|
value = div(voiceCount, er.voiceCount)
|
|
+ div(tupletCount, er.tupletCount);
|
|
}
|
|
return value < 0;
|
|
}
|
|
|
|
private:
|
|
static double div(double val1, double val2)
|
|
{
|
|
if (val1 == val2)
|
|
return 0;
|
|
return (val1 - val2) / qMax(val1, val2);
|
|
}
|
|
|
|
double tupletAverageError;
|
|
double relativeUsedChordPlaces;
|
|
ReducedFraction sumLengthOfRests;
|
|
size_t voiceCount;
|
|
size_t tupletCount;
|
|
};
|
|
|
|
bool haveCommonChords(int i, int j, const std::vector<TupletInfo> &tuplets)
|
|
{
|
|
if (tuplets.empty())
|
|
return false;
|
|
std::set<std::pair<const ReducedFraction, MidiChord> *> chordsI;
|
|
for (const auto &chord: tuplets[i].chords)
|
|
chordsI.insert(&*chord.second);
|
|
for (const auto &chord: tuplets[j].chords)
|
|
if (chordsI.find(&*chord.second) != chordsI.end())
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
// remove overlapping tuplets with the same tuplet number
|
|
// when tuplet with bigger length contains the same notes
|
|
|
|
void removeUselessTuplets(std::vector<TupletInfo> &tuplets)
|
|
{
|
|
struct {
|
|
bool operator()(const TupletInfo &t1, const TupletInfo &t2) {
|
|
if (t1.tupletNumber < t2.tupletNumber)
|
|
return true;
|
|
if (t1.tupletNumber == t2.tupletNumber)
|
|
return t1.len < t2.len;
|
|
return false;
|
|
}
|
|
} comparator;
|
|
std::sort(tuplets.begin(), tuplets.end(), comparator);
|
|
|
|
size_t beg = 0;
|
|
while (beg < tuplets.size()) {
|
|
size_t end = beg + 1;
|
|
while (tuplets.size() > end && tuplets[end].tupletNumber == tuplets[beg].tupletNumber)
|
|
++end;
|
|
for (size_t i = beg; i < end - 1; ++i) {
|
|
const auto &t1 = tuplets[i];
|
|
for (size_t j = i + 1; j < end; ++j) {
|
|
const auto &t2 = tuplets[j];
|
|
if (t1.onTime >= t2.onTime && t1.onTime + t1.len <= t2.onTime + t2.len) {
|
|
// check onTimes
|
|
if (t2.chords.rbegin()->first < t1.onTime + t1.len
|
|
&& t2.chords.begin()->first >= t1.onTime)
|
|
{
|
|
// remove larger tuplet
|
|
tuplets.erase(tuplets.begin() + j);
|
|
--j;
|
|
--end;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
beg = end;
|
|
}
|
|
}
|
|
|
|
std::vector<std::pair<ReducedFraction, ReducedFraction> >
|
|
findTupletIntervals(const std::vector<TupletInfo> &tuplets)
|
|
{
|
|
std::vector<std::pair<ReducedFraction, ReducedFraction>> tupletIntervals;
|
|
for (const auto &tuplet: tuplets)
|
|
tupletIntervals.push_back(tupletInterval(tuplet));
|
|
|
|
return tupletIntervals;
|
|
}
|
|
|
|
std::set<int> findLongestUncommonGroup(const std::vector<TupletInfo> &tuplets)
|
|
{
|
|
struct TInfo
|
|
{
|
|
bool operator<(const TInfo &other) const
|
|
{
|
|
if (offTime < other.offTime)
|
|
return true;
|
|
else if (offTime > other.offTime)
|
|
return false;
|
|
else
|
|
return onTime >= other.onTime;
|
|
}
|
|
bool operator==(const TInfo &other) const
|
|
{
|
|
return offTime == other.offTime;
|
|
}
|
|
|
|
ReducedFraction onTime;
|
|
ReducedFraction offTime;
|
|
int index;
|
|
};
|
|
|
|
std::vector<TInfo> info;
|
|
for (int i = 0; i != (int)tuplets.size(); ++i) {
|
|
const auto &tuplet = tuplets[i];
|
|
info.push_back({tuplet.onTime, tuplet.onTime + tuplet.len, i});
|
|
}
|
|
|
|
std::sort(info.begin(), info.end());
|
|
info.erase(std::unique(info.begin(), info.end()), info.end());
|
|
|
|
// check for overlapping tuplets
|
|
// and check for rare case: because of tol when detecting tuplets
|
|
// non-overlapping tuplets can have common chords
|
|
|
|
std::set<int> indexes;
|
|
int lastSelected = 0;
|
|
for (int i = 0; i != (int)info.size(); ++i) {
|
|
if (i > 0 && info[i].onTime < info[lastSelected].offTime)
|
|
continue;
|
|
if (haveCommonChords(info[lastSelected].index, info[i].index, tuplets))
|
|
continue;
|
|
lastSelected = i;
|
|
indexes.insert(info[i].index);
|
|
}
|
|
|
|
return indexes;
|
|
}
|
|
|
|
|
|
struct TupletCommon
|
|
{
|
|
// indexes of tuplets that have common chords with the tuplet with tupletIndex
|
|
std::set<int> commonIndexes;
|
|
};
|
|
|
|
|
|
std::vector<TupletCommon> findTupletCommons(const std::vector<TupletInfo> &tuplets)
|
|
{
|
|
// <chord address, tuplet indexes>
|
|
std::map<std::pair<const ReducedFraction, MidiChord> *, std::set<int>> usedChords;
|
|
const int voiceLimit = tupletVoiceLimit();
|
|
|
|
for (size_t i = 0; i != tuplets.size(); ++i) {
|
|
const auto &tuplet = tuplets[i];
|
|
auto it = tuplet.chords.begin();
|
|
if (tuplet.firstChordIndex == 0
|
|
&& voiceLimit > 1
|
|
&& it->second->second.notes.size() > 1
|
|
&& usedChords.find(&*it->second) != usedChords.end()) {
|
|
++it;
|
|
}
|
|
for ( ; it != tuplet.chords.end(); ++it)
|
|
usedChords[&*it->second].insert(i);
|
|
}
|
|
|
|
std::vector<TupletCommon> tupletCommons(tuplets.size());
|
|
|
|
for (size_t i = 0; i != tuplets.size(); ++i) {
|
|
for (const auto &chord: tuplets[i].chords) {
|
|
auto &indexes = usedChords[&*chord.second];
|
|
indexes.erase(i);
|
|
tupletCommons[i].commonIndexes.insert(indexes.begin(), indexes.end());
|
|
}
|
|
}
|
|
|
|
return tupletCommons;
|
|
}
|
|
|
|
bool isInCommonIndexes(
|
|
int indexToCheck,
|
|
const std::vector<int> &selectedTuplets,
|
|
const std::vector<TupletCommon> &tupletCommons)
|
|
{
|
|
for (size_t i = 0; i != selectedTuplets.size() - 1; ++i) {
|
|
const auto &indexes = tupletCommons[selectedTuplets[i]].commonIndexes;
|
|
if (indexes.find(indexToCheck) != indexes.end())
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
TupletErrorResult findTupletError(const std::vector<int> &tupletIndexes,
|
|
const std::vector<TupletInfo> &tuplets,
|
|
size_t voiceCount)
|
|
{
|
|
ReducedFraction sumError{0, 1};
|
|
ReducedFraction sumLengthOfRests{0, 1};
|
|
int sumChordCount = 0;
|
|
int sumChordPlaces = 0;
|
|
std::set<std::pair<const ReducedFraction, MidiChord> *> usedChords;
|
|
std::vector<char> usedIndexes(tuplets.size(), 0);
|
|
|
|
for (int i: tupletIndexes) {
|
|
const auto &tuplet = tuplets[i];
|
|
|
|
sumError += tuplet.tupletSumError;
|
|
sumLengthOfRests += tuplet.sumLengthOfRests;
|
|
sumChordCount += tuplet.chords.size();
|
|
sumChordPlaces += tuplet.tupletNumber;
|
|
|
|
usedIndexes[i] = 1;
|
|
for (const auto &chord: tuplet.chords)
|
|
usedChords.insert(&*chord.second);
|
|
}
|
|
// add quant error of all chords excluded from tuplets
|
|
for (size_t i = 0; i != tuplets.size(); ++i) {
|
|
if (usedIndexes[i])
|
|
continue;
|
|
const auto &tuplet = tuplets[i];
|
|
for (const auto &chord: tuplet.chords) {
|
|
if (usedChords.find(&*chord.second) != usedChords.end())
|
|
continue;
|
|
sumError += findQuantizationError(chord.first, tuplet.regularQuant);
|
|
}
|
|
}
|
|
|
|
return TupletErrorResult{
|
|
sumError.numerator() * 1.0 / (sumError.denominator() * sumChordCount),
|
|
sumChordCount * 1.0 / sumChordPlaces,
|
|
sumLengthOfRests,
|
|
voiceCount,
|
|
tupletIndexes.size()
|
|
};
|
|
}
|
|
|
|
|
|
#ifdef QT_DEBUG
|
|
|
|
bool areCommonsDifferent(const std::vector<int> &selectedCommons)
|
|
{
|
|
std::set<int> commons;
|
|
for (int i: selectedCommons) {
|
|
if (commons.find(i) != commons.end())
|
|
return false;
|
|
commons.insert(i);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool areCommonsUncommon(const std::vector<int> &selectedCommons,
|
|
const std::vector<TupletCommon> &tupletCommons)
|
|
{
|
|
std::set<int> commons;
|
|
for (int i: selectedCommons) {
|
|
for (int j: tupletCommons[i].commonIndexes)
|
|
commons.insert(j);
|
|
}
|
|
for (int i: selectedCommons) {
|
|
if (commons.find(i) != commons.end())
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
int findAvailableVoice(
|
|
size_t tupletIndex,
|
|
const std::vector<std::pair<ReducedFraction, ReducedFraction>> &tupletIntervals,
|
|
const std::map<int, std::vector<std::pair<ReducedFraction, ReducedFraction>>> &voiceIntervals)
|
|
{
|
|
int voice = 0;
|
|
while (true) {
|
|
const auto it = voiceIntervals.find(voice);
|
|
if (it != voiceIntervals.end()
|
|
&& haveIntersection(tupletIntervals[tupletIndex], it->second)) {
|
|
++voice;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return voice;
|
|
}
|
|
|
|
std::map<std::pair<const ReducedFraction, MidiChord> *, int>
|
|
prepareUsedFirstChords(const std::vector<int> &selectedTuplets,
|
|
const std::vector<TupletInfo> &tuplets)
|
|
{
|
|
std::map<std::pair<const ReducedFraction, MidiChord> *, int> usedFirstChords;
|
|
for (int i: selectedTuplets) {
|
|
if (tuplets[i].firstChordIndex != 0)
|
|
continue;
|
|
const auto firstChord = tuplets[i].chords.begin();
|
|
const auto it = usedFirstChords.find(&*firstChord->second);
|
|
if (it != usedFirstChords.end())
|
|
++(it->second);
|
|
else
|
|
usedFirstChords.insert({&*firstChord->second, 1});
|
|
}
|
|
|
|
return usedFirstChords;
|
|
}
|
|
|
|
std::vector<int> findUnusedIndexes(const std::vector<int> &selectedTuplets)
|
|
{
|
|
std::vector<int> unusedIndexes;
|
|
int k = 0;
|
|
for (int i = 0; i != selectedTuplets.back(); ++i) {
|
|
if (i == selectedTuplets[k]) {
|
|
++k;
|
|
continue;
|
|
}
|
|
unusedIndexes.push_back(i);
|
|
}
|
|
return unusedIndexes;
|
|
}
|
|
|
|
bool canUseIndex(
|
|
int indexToCheck,
|
|
const std::vector<TupletInfo> &tuplets,
|
|
const std::vector<std::pair<ReducedFraction, ReducedFraction> > &tupletIntervals,
|
|
const std::map<int, std::vector<std::pair<ReducedFraction, ReducedFraction>>> &voiceIntervals,
|
|
const std::map<std::pair<const ReducedFraction, MidiChord> *, int> &usedFirstChords)
|
|
{
|
|
const auto &tuplet = tuplets[indexToCheck];
|
|
// check tuplets for common 1st chord
|
|
if (tuplet.firstChordIndex == 0) {
|
|
const auto firstChord = tuplet.chords.begin();
|
|
const auto it = usedFirstChords.find(&*firstChord->second);
|
|
if (it != usedFirstChords.end() && !isMoreTupletVoicesAllowed(
|
|
it->second, it->first->second.notes.size())) {
|
|
return false;
|
|
}
|
|
}
|
|
// check tuplets for resulting voice count
|
|
const int voice = findAvailableVoice(indexToCheck, tupletIntervals, voiceIntervals);
|
|
const int voiceCount = qMax((int)voiceIntervals.size(), voice + 1);
|
|
if (voiceCount > tupletVoiceLimit() || (voiceCount > 1 && (int)tuplet.chords.size()
|
|
< tupletLimits(tuplet.tupletNumber).minNoteCountAddVoice)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void tryUpdateBestIndexes(
|
|
std::vector<int> &bestTupletIndexes,
|
|
TupletErrorResult &minCurrentError,
|
|
const std::vector<int> &selectedTuplets,
|
|
const std::vector<TupletInfo> &tuplets,
|
|
const std::map<int, std::vector<std::pair<ReducedFraction, ReducedFraction>>> &voiceIntervals)
|
|
{
|
|
const size_t voiceCount = voiceIntervals.size();
|
|
const auto error = findTupletError(selectedTuplets, tuplets, voiceCount);
|
|
if (!minCurrentError.isInitialized() || error < minCurrentError) {
|
|
minCurrentError = error;
|
|
bestTupletIndexes = selectedTuplets;
|
|
}
|
|
}
|
|
|
|
std::map<int, std::vector<std::pair<ReducedFraction, ReducedFraction> > >
|
|
prepareVoiceIntervals(
|
|
const std::vector<int> &selectedTuplets,
|
|
const std::vector<std::pair<ReducedFraction, ReducedFraction> > &tupletIntervals)
|
|
{
|
|
// <voice, intervals>
|
|
std::map<int, std::vector<std::pair<ReducedFraction, ReducedFraction>>> voiceIntervals;
|
|
for (int i: selectedTuplets) {
|
|
int voice = findAvailableVoice(i, tupletIntervals, voiceIntervals);
|
|
voiceIntervals[voice].push_back(tupletIntervals[i]);
|
|
}
|
|
return voiceIntervals;
|
|
}
|
|
|
|
class ValidTuplets
|
|
{
|
|
public:
|
|
ValidTuplets(int tupletsSize)
|
|
: indexes_(tupletsSize)
|
|
, first_(0)
|
|
{
|
|
for (int i = 0; i != (int)indexes_.size(); ++i)
|
|
indexes_[i] = {i - 1, i + 1};
|
|
}
|
|
|
|
int first() const
|
|
{
|
|
return first_;
|
|
}
|
|
|
|
bool isValid(int index) const
|
|
{
|
|
return index >= first_ && index < (int)indexes_.size();
|
|
}
|
|
|
|
int next(int index) const
|
|
{
|
|
return indexes_[index].second;
|
|
}
|
|
|
|
bool empty() const
|
|
{
|
|
return first_ >= (int)indexes_.size();
|
|
}
|
|
|
|
int exclude(int index)
|
|
{
|
|
if (index < first_)
|
|
return index;
|
|
int prev = indexes_[index].first;
|
|
int next = indexes_[index].second;
|
|
indexes_[index].first = -1;
|
|
indexes_[index].second = indexes_.size();
|
|
if (prev >= first_)
|
|
indexes_[prev].second = next;
|
|
if (next < (int)indexes_.size())
|
|
indexes_[next].first = prev;
|
|
if (index == first_)
|
|
first_ = next;
|
|
return next;
|
|
}
|
|
|
|
std::vector<std::pair<int, int>> save()
|
|
{
|
|
std::vector<std::pair<int, int>> indexes(indexes_.size() - first_);
|
|
for (int i = first_; i != (int)indexes_.size(); ++i)
|
|
indexes[i - first_] = indexes_[i];
|
|
return indexes;
|
|
}
|
|
|
|
void restore(const std::vector<std::pair<int, int>> &indexes)
|
|
{
|
|
first_ = indexes_.size() - indexes.size();
|
|
for (int i = 0; i != (int)indexes.size(); ++i)
|
|
indexes_[i + first_] = indexes[i];
|
|
}
|
|
|
|
private:
|
|
std::vector<std::pair<int, int>> indexes_; // pair<prev, next>
|
|
int first_;
|
|
};
|
|
|
|
|
|
void findNextTuplet(
|
|
std::vector<int> &selectedTuplets,
|
|
ValidTuplets &validTuplets,
|
|
std::vector<int> &bestTupletIndexes,
|
|
TupletErrorResult &minCurrentError,
|
|
const std::vector<TupletCommon> &tupletCommons,
|
|
const std::vector<TupletInfo> &tuplets,
|
|
const std::vector<std::pair<ReducedFraction, ReducedFraction> > &tupletIntervals,
|
|
size_t commonsSize)
|
|
{
|
|
while (!validTuplets.empty()) {
|
|
size_t index = validTuplets.first();
|
|
|
|
bool isCommonGroupBegins = (selectedTuplets.empty() && index == commonsSize);
|
|
if (isCommonGroupBegins) { // first level
|
|
for (size_t i = index; i < tuplets.size(); ++i)
|
|
selectedTuplets.push_back(i);
|
|
}
|
|
else {
|
|
selectedTuplets.push_back(index);
|
|
}
|
|
const auto voiceIntervals = prepareVoiceIntervals(selectedTuplets, tupletIntervals);
|
|
const auto usedFirstChords = prepareUsedFirstChords(selectedTuplets, tuplets);
|
|
|
|
Q_ASSERT_X(areCommonsDifferent(selectedTuplets), "MidiTuplet::findNextTuplet",
|
|
"There are duplicates in selected commons");
|
|
Q_ASSERT_X(areCommonsUncommon(selectedTuplets, tupletCommons),
|
|
"MidiTuplet::findNextTuplet", "Incompatible selected commons");
|
|
|
|
if (isCommonGroupBegins) {
|
|
bool canAddMoreIndexes = false;
|
|
for (size_t i = 0; i != commonsSize; ++i) {
|
|
if (!isInCommonIndexes(i, selectedTuplets, tupletCommons)
|
|
&& canUseIndex(i, tuplets, tupletIntervals,
|
|
voiceIntervals, usedFirstChords)) {
|
|
canAddMoreIndexes = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!canAddMoreIndexes) {
|
|
tryUpdateBestIndexes(bestTupletIndexes, minCurrentError,
|
|
selectedTuplets, tuplets, voiceIntervals);
|
|
}
|
|
return;
|
|
}
|
|
|
|
validTuplets.exclude(index);
|
|
const auto savedTuplets = validTuplets.save();
|
|
// check tuplets for compatibility
|
|
if (!validTuplets.empty()) {
|
|
for (int i: tupletCommons[index].commonIndexes) {
|
|
validTuplets.exclude(i);
|
|
if (validTuplets.empty())
|
|
break;
|
|
}
|
|
}
|
|
for (int i = validTuplets.first(); validTuplets.isValid(i); ) {
|
|
if (!canUseIndex(i, tuplets, tupletIntervals,
|
|
voiceIntervals, usedFirstChords)) {
|
|
i = validTuplets.exclude(i);
|
|
continue;
|
|
}
|
|
i = validTuplets.next(i);
|
|
}
|
|
if (validTuplets.empty()) {
|
|
const auto unusedIndexes = findUnusedIndexes(selectedTuplets);
|
|
bool canAddMoreIndexes = false;
|
|
for (int i: unusedIndexes) {
|
|
if (!isInCommonIndexes(i, selectedTuplets, tupletCommons)
|
|
&& canUseIndex(i, tuplets, tupletIntervals,
|
|
voiceIntervals, usedFirstChords)) {
|
|
canAddMoreIndexes = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!canAddMoreIndexes) {
|
|
tryUpdateBestIndexes(bestTupletIndexes, minCurrentError,
|
|
selectedTuplets, tuplets, voiceIntervals);
|
|
}
|
|
}
|
|
else {
|
|
findNextTuplet(selectedTuplets, validTuplets, bestTupletIndexes, minCurrentError,
|
|
tupletCommons, tuplets, tupletIntervals, commonsSize);
|
|
}
|
|
|
|
selectedTuplets.pop_back();
|
|
validTuplets.restore(savedTuplets);
|
|
}
|
|
}
|
|
|
|
void moveUncommonTupletsToEnd(std::vector<TupletInfo> &tuplets, std::set<int> &uncommons)
|
|
{
|
|
int swapWith = tuplets.size() - 1;
|
|
for (int i = swapWith; i >= 0; --i) {
|
|
auto it = uncommons.find(i);
|
|
if (it != uncommons.end()) {
|
|
if (i != swapWith)
|
|
std::swap(tuplets[i], tuplets[swapWith]);
|
|
--swapWith;
|
|
uncommons.erase(it);
|
|
}
|
|
}
|
|
|
|
Q_ASSERT_X(uncommons.empty(), "MidiTuplet::moveUncommonTupletsToEnd",
|
|
"Untested uncommon tuplets remaining");
|
|
}
|
|
|
|
std::vector<int> findBestTuplets(
|
|
const std::vector<TupletCommon> &tupletCommons,
|
|
const std::vector<TupletInfo> &tuplets,
|
|
size_t commonsSize)
|
|
{
|
|
std::vector<int> bestTupletIndexes;
|
|
std::vector<int> selectedTuplets;
|
|
TupletErrorResult minCurrentError;
|
|
const auto tupletIntervals = findTupletIntervals(tuplets);
|
|
|
|
ValidTuplets validTuplets(tuplets.size());
|
|
|
|
findNextTuplet(selectedTuplets, validTuplets, bestTupletIndexes, minCurrentError,
|
|
tupletCommons, tuplets, tupletIntervals, commonsSize);
|
|
|
|
return bestTupletIndexes;
|
|
}
|
|
|
|
|
|
#ifdef QT_DEBUG
|
|
|
|
bool areTupletChordsEmpty(const std::vector<TupletInfo> &tuplets)
|
|
{
|
|
for (const auto &tuplet: tuplets) {
|
|
if (tuplet.chords.empty())
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
template<typename Iter>
|
|
bool validateSelectedTuplets(Iter beginIt,
|
|
Iter endIt,
|
|
const std::vector<TupletInfo> &tuplets)
|
|
{
|
|
// <chord address, used voices>
|
|
std::map<std::pair<const ReducedFraction, MidiChord> *, int> usedChords;
|
|
for (auto it = beginIt; it != endIt; ++it) {
|
|
const auto &tuplet = tuplets[*it];
|
|
const auto &chords = tuplet.chords;
|
|
for (auto it = chords.begin(); it != chords.end(); ++it) {
|
|
bool isFirstChord = (tuplet.firstChordIndex == 0 && it == tuplet.chords.begin());
|
|
const auto fit = usedChords.find(&*(it->second));
|
|
if (fit == usedChords.end()) {
|
|
usedChords.insert({&*(it->second), isFirstChord ? 1 : VOICES});
|
|
}
|
|
else {
|
|
if (!isFirstChord)
|
|
return false;
|
|
if (!isMoreTupletVoicesAllowed(fit->second, it->second->second.notes.size()))
|
|
return false;
|
|
++(fit->second);
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#endif
|
|
|
|
void removeExtraTuplets(std::vector<TupletInfo> &tuplets)
|
|
{
|
|
const size_t MAX_TUPLETS = 23; // found empirically
|
|
|
|
if (tuplets.size() <= MAX_TUPLETS)
|
|
return;
|
|
|
|
std::map<TupletErrorResult, size_t> errors;
|
|
for (size_t i = 0; i != tuplets.size(); ++i) {
|
|
auto tupletError = TupletErrorResult{
|
|
tuplets[i].tupletSumError.numerator() * 1.0
|
|
/ (tuplets[i].tupletSumError.denominator() * tuplets[i].chords.size()),
|
|
tuplets[i].chords.size() * 1.0 / tuplets[i].tupletNumber,
|
|
tuplets[i].sumLengthOfRests,
|
|
1,
|
|
1
|
|
};
|
|
errors.insert({tupletError, i});
|
|
}
|
|
std::vector<TupletInfo> newTuplets;
|
|
size_t count = 0;
|
|
for (const auto &e: errors) {
|
|
++count;
|
|
newTuplets.push_back(tuplets[e.second]);
|
|
if (count == MAX_TUPLETS)
|
|
break;
|
|
}
|
|
|
|
std::swap(tuplets, newTuplets);
|
|
}
|
|
|
|
|
|
// first chord in tuplet may belong to other tuplet at the same time
|
|
// in the case if there are enough notes in this first chord
|
|
// to be splitted into different voices
|
|
|
|
void filterTuplets(std::vector<TupletInfo> &tuplets)
|
|
{
|
|
if (tuplets.empty())
|
|
return;
|
|
|
|
Q_ASSERT_X(!areTupletChordsEmpty(tuplets),
|
|
"MIDI tuplets: filterTuplets", "Tuplet has no chords but it should");
|
|
|
|
removeUselessTuplets(tuplets);
|
|
removeExtraTuplets(tuplets);
|
|
|
|
std::set<int> uncommons = findLongestUncommonGroup(tuplets);
|
|
|
|
Q_ASSERT_X(validateSelectedTuplets(uncommons.begin(), uncommons.end(), tuplets),
|
|
"MIDI tuplets: filterTuplets",
|
|
"Uncommon tuplets have common chords but they shouldn't");
|
|
|
|
size_t commonsSize = tuplets.size();
|
|
if (uncommons.size() > 1) {
|
|
commonsSize -= uncommons.size();
|
|
moveUncommonTupletsToEnd(tuplets, uncommons);
|
|
}
|
|
const auto tupletCommons = findTupletCommons(tuplets);
|
|
|
|
const std::vector<int> bestIndexes = findBestTuplets(tupletCommons, tuplets, commonsSize);
|
|
|
|
Q_ASSERT_X(validateSelectedTuplets(bestIndexes.begin(), bestIndexes.end(), tuplets),
|
|
"MIDI tuplets: filterTuplets", "Tuplets have common chords but they shouldn't");
|
|
|
|
std::vector<TupletInfo> newTuplets;
|
|
for (int i: bestIndexes)
|
|
newTuplets.push_back(tuplets[i]);
|
|
|
|
std::swap(tuplets, newTuplets);
|
|
}
|
|
|
|
int averagePitch(const std::map<ReducedFraction,
|
|
std::multimap<ReducedFraction, MidiChord>::iterator> &chords)
|
|
{
|
|
if (chords.empty())
|
|
return -1;
|
|
int sumPitch = 0;
|
|
int noteCounter = 0;
|
|
for (const auto &chord: chords) {
|
|
const auto &midiNotes = chord.second->second.notes;
|
|
for (const auto &midiNote: midiNotes) {
|
|
sumPitch += midiNote.pitch;
|
|
++noteCounter;
|
|
}
|
|
}
|
|
return sumPitch / noteCounter;
|
|
}
|
|
|
|
int averagePitch(const std::list<std::multimap<ReducedFraction, MidiChord>::iterator> &chords)
|
|
{
|
|
if (chords.empty())
|
|
return -1;
|
|
int sumPitch = 0;
|
|
int noteCounter = 0;
|
|
for (const auto &it: chords) {
|
|
const auto &midiNotes = it->second.notes;
|
|
for (const auto &midiNote: midiNotes) {
|
|
sumPitch += midiNote.pitch;
|
|
++noteCounter;
|
|
}
|
|
}
|
|
return sumPitch / noteCounter;
|
|
}
|
|
|
|
void sortNotesByPitch(const std::multimap<ReducedFraction, MidiChord>::iterator &startBarChordIt,
|
|
const std::multimap<ReducedFraction, MidiChord>::iterator &endBarChordIt)
|
|
{
|
|
struct {
|
|
bool operator()(const MidiNote &n1, const MidiNote &n2)
|
|
{
|
|
return (n1.pitch > n2.pitch);
|
|
}
|
|
} pitchComparator;
|
|
|
|
for (auto it = startBarChordIt; it != endBarChordIt; ++it) {
|
|
auto &midiNotes = it->second.notes;
|
|
std::sort(midiNotes.begin(), midiNotes.end(), pitchComparator);
|
|
}
|
|
}
|
|
|
|
void sortTupletsByAveragePitch(std::vector<TupletInfo> &tuplets)
|
|
{
|
|
struct {
|
|
bool operator()(const TupletInfo &t1, const TupletInfo &t2)
|
|
{
|
|
return (averagePitch(t1.chords) > averagePitch(t2.chords));
|
|
}
|
|
} averagePitchComparator;
|
|
std::sort(tuplets.begin(), tuplets.end(), averagePitchComparator);
|
|
}
|
|
|
|
std::set<std::pair<const ReducedFraction, MidiChord> *>
|
|
findTupletChords(const std::vector<TupletInfo> &tuplets)
|
|
{
|
|
std::set<std::pair<const ReducedFraction, MidiChord> *> tupletChords;
|
|
for (const auto &tupletInfo: tuplets) {
|
|
for (const auto &tupletChord: tupletInfo.chords) {
|
|
auto tupletIt = tupletChord.second;
|
|
tupletChords.insert(&*tupletIt);
|
|
}
|
|
}
|
|
return tupletChords;
|
|
}
|
|
|
|
std::map<std::pair<const ReducedFraction, MidiChord> *, int>
|
|
findMappedTupletChords(const std::vector<TupletInfo> &tuplets)
|
|
{
|
|
// <chord address, tupletIndex>
|
|
std::map<std::pair<const ReducedFraction, MidiChord> *, int> tupletChords;
|
|
for (int i = 0; i != (int)tuplets.size(); ++i) {
|
|
for (const auto &tupletChord: tuplets[i].chords) {
|
|
auto tupletIt = tupletChord.second;
|
|
tupletChords.insert({&*tupletIt, i});
|
|
}
|
|
}
|
|
return tupletChords;
|
|
}
|
|
|
|
std::list<std::multimap<ReducedFraction, MidiChord>::iterator>
|
|
findNonTupletChords(const std::vector<TupletInfo> &tuplets,
|
|
const std::multimap<ReducedFraction, MidiChord>::iterator &startBarChordIt,
|
|
const std::multimap<ReducedFraction, MidiChord>::iterator &endBarChordIt)
|
|
{
|
|
const auto tupletChords = findTupletChords(tuplets);
|
|
std::list<std::multimap<ReducedFraction, MidiChord>::iterator> nonTuplets;
|
|
for (auto it = startBarChordIt; it != endBarChordIt; ++it) {
|
|
if (tupletChords.find(&*it) == tupletChords.end())
|
|
nonTuplets.push_back(it);
|
|
}
|
|
|
|
return nonTuplets;
|
|
}
|
|
|
|
// do quantization before checking
|
|
|
|
bool hasIntersectionWithChord(const ReducedFraction &startTick,
|
|
const ReducedFraction &endTick,
|
|
const ReducedFraction ®ularRaster,
|
|
const std::multimap<ReducedFraction, MidiChord>::iterator &chord)
|
|
{
|
|
const auto onTimeRaster = Quantize::reduceRasterIfDottedNote(
|
|
MChord::maxNoteOffTime(chord->second.notes) - chord->first, regularRaster);
|
|
// don't need to measure time values from bar start
|
|
// because of regular raster (not tuplet raster)
|
|
const auto onTime = Quantize::quantizeValue(chord->first, onTimeRaster);
|
|
MidiChord testChord = chord->second; // copy chord
|
|
for (auto ¬e: testChord.notes) {
|
|
const auto offTimeRaster = Quantize::reduceRasterIfDottedNote(
|
|
note.offTime - chord->first, regularRaster);
|
|
note.offTime = Quantize::quantizeValue(note.offTime, offTimeRaster);
|
|
}
|
|
return (endTick > onTime && startTick < MChord::maxNoteOffTime(testChord.notes));
|
|
}
|
|
|
|
// split first tuplet chord, that belong to 2 tuplets, into 2 chords
|
|
|
|
void splitTupletChord(const std::vector<TupletInfo>::iterator &lastMatch,
|
|
std::multimap<ReducedFraction, MidiChord> &chords)
|
|
{
|
|
auto &chordEvent = lastMatch->chords.begin()->second;
|
|
MidiChord &prevChord = chordEvent->second;
|
|
const auto onTime = chordEvent->first;
|
|
MidiChord newChord = prevChord;
|
|
// erase all notes except the first one
|
|
auto beg = newChord.notes.begin();
|
|
newChord.notes.erase(++beg, newChord.notes.end());
|
|
// erase the first note
|
|
prevChord.notes.erase(prevChord.notes.begin());
|
|
chordEvent = chords.insert({onTime, newChord});
|
|
if (prevChord.notes.isEmpty()) {
|
|
// normally this should not happen at all because of filtering of tuplets
|
|
qDebug("Tuplets were not filtered correctly: same notes in different tuplets");
|
|
}
|
|
}
|
|
|
|
void setTupletVoice(std::map<ReducedFraction, std::multimap<ReducedFraction, MidiChord>::iterator> &tupletChords,
|
|
int voice)
|
|
{
|
|
for (auto &tupletChord: tupletChords) {
|
|
MidiChord &midiChord = tupletChord.second->second;
|
|
midiChord.voice = voice;
|
|
midiChord.isInTuplet = true;
|
|
}
|
|
}
|
|
|
|
void splitFirstTupletChords(std::vector<TupletInfo> &tuplets,
|
|
std::multimap<ReducedFraction, MidiChord> &chords)
|
|
{
|
|
for (auto now = tuplets.begin(); now != tuplets.end(); ++now) {
|
|
auto lastMatch = tuplets.end();
|
|
const auto nowChordIt = now->chords.begin();
|
|
for (auto prev = tuplets.begin(); prev != now; ++prev) {
|
|
auto prevChordIt = prev->chords.begin();
|
|
if (now->firstChordIndex == 0
|
|
&& prev->firstChordIndex == 0
|
|
&& nowChordIt->second == prevChordIt->second) {
|
|
lastMatch = prev;
|
|
}
|
|
}
|
|
if (lastMatch != tuplets.end())
|
|
splitTupletChord(lastMatch, chords);
|
|
}
|
|
}
|
|
|
|
bool canTupletBeTied(const TupletInfo &tuplet,
|
|
const std::multimap<ReducedFraction, MidiChord>::iterator &chordIt,
|
|
const ReducedFraction &startBarTick)
|
|
{
|
|
const auto chordOffTime = MChord::maxNoteOffTime(chordIt->second.notes);
|
|
const auto raster = Quantize::reduceRasterIfDottedNote(chordOffTime - chordIt->first,
|
|
tuplet.regularQuant);
|
|
const auto onTime = Quantize::quantizeValue(chordIt->first, raster);
|
|
if (onTime >= tuplet.onTime)
|
|
return false;
|
|
|
|
const auto tupletOffTime = startBarTick + Quantize::quantizeValue(
|
|
chordOffTime - startBarTick, tuplet.tupletQuant);
|
|
// if chord offTime is outside this bar or tuplet - discard chord
|
|
if (tupletOffTime < startBarTick
|
|
|| tupletOffTime <= tuplet.onTime
|
|
|| tupletOffTime >= tuplet.onTime + tuplet.len)
|
|
return false;
|
|
|
|
const auto regularError = findQuantizationError(chordOffTime, raster);
|
|
const auto tupletError = (chordOffTime - tupletOffTime).absValue();
|
|
|
|
if (tupletError <= regularError)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
ReducedFraction findPrevBarStart(const ReducedFraction &barStart,
|
|
const ReducedFraction &barLen)
|
|
{
|
|
auto prevBarStart = barStart - barLen;
|
|
if (prevBarStart < ReducedFraction(0, 1))
|
|
prevBarStart = ReducedFraction(0, 1);
|
|
return prevBarStart;
|
|
}
|
|
|
|
struct TiedTuplet
|
|
{
|
|
int tupletIndex;
|
|
int voice;
|
|
std::pair<const ReducedFraction, MidiChord> *chord; // chord the tuplet is tied with
|
|
};
|
|
|
|
std::vector<TiedTuplet>
|
|
findBackTiedTuplets(
|
|
const std::multimap<ReducedFraction, MidiChord> &chords,
|
|
const std::vector<TupletInfo> &tuplets,
|
|
const ReducedFraction &prevBarStart,
|
|
const ReducedFraction &startBarTick)
|
|
{
|
|
std::vector<TiedTuplet> tiedTuplets;
|
|
const auto tupletChords = findMappedTupletChords(tuplets);
|
|
const auto tupletIntervals = findTupletIntervals(tuplets);
|
|
|
|
for (int i = 0; i != (int)tuplets.size(); ++i) {
|
|
|
|
Q_ASSERT_X(!tuplets[i].chords.empty(),
|
|
"MidiTuplets::findBackTiedTuplets", "Tuplet chords are empty");
|
|
|
|
auto chordIt = tuplets[i].chords.begin()->second;
|
|
while (chordIt != chords.begin() && chordIt->first >= prevBarStart) {
|
|
--chordIt;
|
|
int voice = chordIt->second.voice; // voice can be from prev bar also
|
|
bool used = false;
|
|
// check: if tuplet 'i' already tied then voices must match
|
|
for (const auto &t: tiedTuplets) {
|
|
if (t.tupletIndex == i && t.voice != voice) {
|
|
used = true;
|
|
break;
|
|
}
|
|
if (t.tupletIndex != i && t.voice == voice) {
|
|
if (haveIntersection(tupletIntervals[i],
|
|
tupletIntervals[t.tupletIndex])) {
|
|
used = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (used)
|
|
continue;
|
|
// don't make back tie to the chord in overlapping tuplet
|
|
const auto tupletIt = tupletChords.find(&*chordIt);
|
|
if (tupletIt != tupletChords.end()) {
|
|
const auto onTime1 = tuplets[tupletIt->second].onTime;
|
|
const auto endTime1 = onTime1 + tuplets[tupletIt->second].len;
|
|
const auto onTime2 = tuplets[i].onTime;
|
|
const auto endTime2 = onTime1 + tuplets[i].len;
|
|
if (endTime1 > onTime2 && onTime1 < endTime2) // tuplet intersection
|
|
continue;
|
|
}
|
|
if (canTupletBeTied(tuplets[i], chordIt, startBarTick)) {
|
|
tiedTuplets.push_back({i, voice, &*chordIt});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return tiedTuplets;
|
|
}
|
|
|
|
// first tuplet notes with offTime quantization error,
|
|
// that is greater for tuplet quant rather than for regular quant,
|
|
// are removed from tuplet, except that was the last note
|
|
|
|
// if noteLen <= tupletLen
|
|
// remove notes with big offTime quant error;
|
|
// and here is a tuning for clearer tuplet processing:
|
|
// if tuplet has only one (first) chord -
|
|
// we can remove all notes and erase tuplet;
|
|
// if tuplet has multiple chords -
|
|
// we should leave at least one note in the first chord
|
|
|
|
void minimizeOffTimeError(std::vector<TupletInfo> &tuplets,
|
|
std::multimap<ReducedFraction, MidiChord> &chords,
|
|
std::list<std::multimap<ReducedFraction, MidiChord>::iterator> &nonTuplets,
|
|
const ReducedFraction &startBarTick)
|
|
{
|
|
for (auto it = tuplets.begin(); it != tuplets.end(); ) {
|
|
TupletInfo &tupletInfo = *it;
|
|
const auto firstChord = tupletInfo.chords.begin();
|
|
if (firstChord == tupletInfo.chords.end() || tupletInfo.firstChordIndex != 0) {
|
|
++it;
|
|
continue;
|
|
}
|
|
auto onTime = firstChord->second->first;
|
|
// because of tol onTime can be less than start bar tick
|
|
if (onTime < startBarTick)
|
|
onTime = startBarTick;
|
|
MidiChord &midiChord = firstChord->second->second;
|
|
auto ¬es = midiChord.notes;
|
|
|
|
std::vector<int> removedIndexes;
|
|
std::vector<int> leavedIndexes;
|
|
for (int i = 0; i != notes.size(); ++i) {
|
|
const auto ¬e = notes[i];
|
|
if (note.offTime - onTime <= tupletInfo.len) {
|
|
if ((tupletInfo.chords.size() == 1
|
|
&& notes.size() > (int)removedIndexes.size())
|
|
|| (tupletInfo.chords.size() > 1
|
|
&& notes.size() > (int)removedIndexes.size() + 1))
|
|
{
|
|
const auto tupletError = findQuantizationError(
|
|
note.offTime - startBarTick, tupletInfo.tupletQuant);
|
|
const auto regularRaster = Quantize::reduceRasterIfDottedNote(
|
|
note.offTime - onTime, tupletInfo.regularQuant);
|
|
const auto regularError = findQuantizationError(
|
|
note.offTime, regularRaster);
|
|
if (tupletError > regularError) {
|
|
removedIndexes.push_back(i);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
leavedIndexes.push_back(i);
|
|
}
|
|
if (!removedIndexes.empty()) {
|
|
MidiChord newTupletChord;
|
|
for (int i: leavedIndexes)
|
|
newTupletChord.notes.push_back(notes[i]);
|
|
|
|
QList<MidiNote> newNotes;
|
|
for (int i: removedIndexes)
|
|
newNotes.push_back(notes[i]);
|
|
notes = newNotes;
|
|
nonTuplets.push_back(firstChord->second);
|
|
if (!newTupletChord.notes.empty())
|
|
firstChord->second = chords.insert({onTime, newTupletChord});
|
|
else {
|
|
tupletInfo.chords.erase(tupletInfo.chords.begin());
|
|
if (tupletInfo.chords.empty()) {
|
|
it = tuplets.erase(it); // remove tuplet without chords
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
++it;
|
|
}
|
|
}
|
|
|
|
void addChordsBetweenTupletNotes(
|
|
std::vector<TupletInfo> &tuplets,
|
|
std::list<std::multimap<ReducedFraction, MidiChord>::iterator> &nonTuplets,
|
|
const ReducedFraction &startBarTick)
|
|
{
|
|
for (TupletInfo &tuplet: tuplets) {
|
|
for (auto it = nonTuplets.begin(); it != nonTuplets.end(); ) {
|
|
const auto &chordIt = *it;
|
|
const auto &onTime = chordIt->first;
|
|
if (onTime > tuplet.onTime
|
|
&& hasIntersectionWithChord(tuplet.onTime, tuplet.onTime + tuplet.len,
|
|
tuplet.regularQuant, chordIt)) {
|
|
auto regularError = findQuantizationError(onTime, tuplet.regularQuant);
|
|
auto tupletError = findQuantizationError(
|
|
onTime - startBarTick, tuplet.tupletQuant);
|
|
const auto offTime = MChord::maxNoteOffTime(chordIt->second.notes);
|
|
if (offTime < tuplet.onTime + tuplet.len) {
|
|
const auto regularRaster = Quantize::reduceRasterIfDottedNote(
|
|
offTime - onTime, tuplet.regularQuant);
|
|
regularError += findQuantizationError(offTime, regularRaster);
|
|
tupletError += findQuantizationError(
|
|
offTime - startBarTick, tuplet.tupletQuant);
|
|
}
|
|
if (tupletError < regularError) {
|
|
tuplet.chords.insert({onTime, chordIt});
|
|
it = nonTuplets.erase(it);
|
|
continue;
|
|
}
|
|
}
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::pair<ReducedFraction, ReducedFraction>
|
|
chordInterval(const std::pair<const ReducedFraction, MidiChord> *chord,
|
|
const ReducedFraction ®ularRaster)
|
|
{
|
|
// don't need to measure time values from bar start
|
|
// because we use here regular raster, not tuplet raster
|
|
auto onTime = Quantize::quantizeValue(chord->first, regularRaster);
|
|
auto offTime = MChord::maxNoteOffTime(chord->second.notes);
|
|
const auto raster = Quantize::reduceRasterIfDottedNote(offTime - chord->first, regularRaster);
|
|
offTime = Quantize::quantizeValue(offTime, raster);
|
|
return std::make_pair(onTime, offTime);
|
|
}
|
|
|
|
void setTupletVoices(std::vector<TupletInfo> &tuplets,
|
|
std::set<int> &pendingTuplets,
|
|
std::map<int, std::vector<std::pair<ReducedFraction, ReducedFraction>>> &tupletIntervals)
|
|
{
|
|
int limit = tupletVoiceLimit();
|
|
int voice = 0;
|
|
while (!pendingTuplets.empty() && voice < limit) {
|
|
for (auto it = pendingTuplets.begin(); it != pendingTuplets.end(); ) {
|
|
int i = *it;
|
|
const auto interval = tupletInterval(tuplets[i]);
|
|
if (!haveIntersection(interval, tupletIntervals[voice])) {
|
|
setTupletVoice(tuplets[i].chords, voice);
|
|
tupletIntervals[voice].push_back(interval);
|
|
it = pendingTuplets.erase(it);
|
|
continue;
|
|
}
|
|
++it;
|
|
}
|
|
++voice;
|
|
}
|
|
}
|
|
|
|
void setNonTupletVoices(std::set<std::pair<const ReducedFraction, MidiChord> *> &pendingNonTuplets,
|
|
const std::map<int, std::vector<std::pair<ReducedFraction, ReducedFraction>>> &tupletIntervals,
|
|
const ReducedFraction ®ularRaster)
|
|
{
|
|
const int limit = voiceLimit();
|
|
int voice = 0;
|
|
while (!pendingNonTuplets.empty() && voice < limit) {
|
|
for (auto it = pendingNonTuplets.begin(); it != pendingNonTuplets.end(); ) {
|
|
auto chord = *it;
|
|
const auto interval = chordInterval(chord, regularRaster);
|
|
const auto fit = tupletIntervals.find(voice);
|
|
if (fit == tupletIntervals.end() || !haveIntersection(interval, fit->second)) {
|
|
chord->second.voice = voice;
|
|
it = pendingNonTuplets.erase(it);
|
|
// don't insert chord interval here
|
|
continue;
|
|
}
|
|
++it;
|
|
}
|
|
++voice;
|
|
}
|
|
}
|
|
|
|
void removeUnusedTuplets(std::vector<TupletInfo> &tuplets,
|
|
std::list<std::multimap<ReducedFraction, MidiChord>::iterator> &nonTuplets,
|
|
std::set<int> &pendingTuplets,
|
|
std::set<std::pair<const ReducedFraction, MidiChord> *> &pendingNonTuplets)
|
|
{
|
|
if (pendingTuplets.empty())
|
|
return;
|
|
|
|
std::vector<TupletInfo> newTuplets;
|
|
for (int i = 0; i != (int)tuplets.size(); ++i) {
|
|
if (pendingTuplets.find(i) == pendingTuplets.end()) {
|
|
newTuplets.push_back(tuplets[i]);
|
|
}
|
|
else {
|
|
for (const auto &chord: tuplets[i].chords) {
|
|
nonTuplets.push_back(chord.second);
|
|
pendingNonTuplets.insert(&*chord.second);
|
|
}
|
|
}
|
|
}
|
|
pendingTuplets.clear();
|
|
std::swap(tuplets, newTuplets);
|
|
}
|
|
|
|
|
|
#ifdef QT_DEBUG
|
|
|
|
bool haveTupletsEmptyChords(const std::vector<TupletInfo> &tuplets)
|
|
{
|
|
for (const auto &tuplet: tuplets) {
|
|
if (tuplet.chords.empty())
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool doTupletChordsHaveSameVoice(const std::vector<TupletInfo> &tuplets)
|
|
{
|
|
for (const auto &tuplet: tuplets) {
|
|
auto it = tuplet.chords.cbegin();
|
|
const int voice = it->second->second.voice;
|
|
++it;
|
|
for ( ; it != tuplet.chords.cend(); ++it) {
|
|
if (it->second->second.voice != voice)
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// back tied tuplets are not checked here
|
|
|
|
bool haveOverlappingVoices(const std::list<std::multimap<ReducedFraction, MidiChord>::iterator> &nonTuplets,
|
|
const std::vector<TupletInfo> &tuplets,
|
|
const ReducedFraction ®ularRaster,
|
|
const std::vector<TiedTuplet> &backTiedTuplets = std::vector<TiedTuplet>())
|
|
{
|
|
// <voice, intervals>
|
|
std::map<int, std::vector<std::pair<ReducedFraction, ReducedFraction>>> intervals;
|
|
|
|
for (const auto &tuplet: tuplets) {
|
|
const int voice = tuplet.chords.begin()->second->second.voice;
|
|
const auto interval = std::make_pair(tuplet.onTime, tuplet.onTime + tuplet.len);
|
|
if (haveIntersection(interval, intervals[voice]))
|
|
return true;
|
|
else
|
|
intervals[voice].push_back(interval);
|
|
}
|
|
|
|
for (const auto &chord: nonTuplets) {
|
|
const int voice = chord->second.voice;
|
|
const auto interval = chordInterval(&*chord, regularRaster);
|
|
if (haveIntersection(interval, intervals[voice])) {
|
|
bool flag = false; // if chord is tied then it can intersect tuplet
|
|
for (const TiedTuplet &tiedTuplet: backTiedTuplets) {
|
|
if (tiedTuplet.chord == (&*chord) && tiedTuplet.voice == voice) {
|
|
flag = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!flag)
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool doTupletsHaveCommonChords(const std::vector<TupletInfo> &tuplets)
|
|
{
|
|
if (tuplets.empty())
|
|
return false;
|
|
std::set<std::pair<const ReducedFraction, MidiChord> *> chordsI;
|
|
for (const auto &tuplet: tuplets) {
|
|
for (const auto &chord: tuplet.chords) {
|
|
if (chordsI.find(&*chord.second) != chordsI.end())
|
|
return true;
|
|
chordsI.insert(&*chord.second);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
std::vector<std::pair<ReducedFraction, ReducedFraction> >
|
|
findNonTupletIntervals(const std::list<std::multimap<ReducedFraction, MidiChord>::iterator> &nonTuplets,
|
|
const ReducedFraction ®ularRaster)
|
|
{
|
|
std::vector<std::pair<ReducedFraction, ReducedFraction>> nonTupletIntervals;
|
|
for (const auto &nonTuplet: nonTuplets) {
|
|
nonTupletIntervals.push_back(chordInterval(&*nonTuplet, regularRaster));
|
|
}
|
|
return nonTupletIntervals;
|
|
}
|
|
|
|
std::set<int> findPendingTuplets(const std::vector<TupletInfo> &tuplets)
|
|
{
|
|
std::set<int> pendingTuplets; // tuplet indexes
|
|
for (int i = 0; i != (int)tuplets.size(); ++i) {
|
|
pendingTuplets.insert(i);
|
|
}
|
|
return pendingTuplets;
|
|
}
|
|
|
|
std::set<std::pair<const ReducedFraction, MidiChord> *>
|
|
findPendingNonTuplets(const std::list<std::multimap<ReducedFraction, MidiChord>::iterator> &nonTuplets)
|
|
{
|
|
std::set<std::pair<const ReducedFraction, MidiChord> *> pendingNonTuplets;
|
|
for (const auto &c: nonTuplets) {
|
|
pendingNonTuplets.insert(&*c);
|
|
}
|
|
return pendingNonTuplets;
|
|
}
|
|
|
|
int findTupletWithChord(const MidiChord &midiChord,
|
|
const std::vector<TupletInfo> &tuplets)
|
|
{
|
|
for (int i = 0; i != (int)tuplets.size(); ++i) {
|
|
for (const auto &chord: tuplets[i].chords) {
|
|
if (&(chord.second->second) == &midiChord)
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
std::vector<std::pair<int, int> >
|
|
findForTiedTuplets(const std::vector<TupletInfo> &tuplets,
|
|
const std::vector<TiedTuplet> &tiedTuplets,
|
|
const std::set<std::pair<const ReducedFraction, MidiChord> *> &pendingNonTuplets,
|
|
const ReducedFraction &startBarTick)
|
|
{
|
|
std::vector<std::pair<int, int>> forTiedTuplets; // <tuplet index, voice to assign>
|
|
|
|
for (const TiedTuplet &tuplet: tiedTuplets) {
|
|
// only for chords in the current bar (because of tol some can be outside)
|
|
if (tuplet.chord->first < startBarTick)
|
|
continue;
|
|
if (pendingNonTuplets.find(tuplet.chord) == pendingNonTuplets.end()) {
|
|
const int i = findTupletWithChord(tuplet.chord->second, tuplets);
|
|
if (i != -1)
|
|
forTiedTuplets.push_back({i, tuplet.voice});
|
|
}
|
|
}
|
|
return forTiedTuplets;
|
|
}
|
|
|
|
|
|
#ifdef QT_DEBUG
|
|
|
|
bool areAllElementsUnique(const std::list<std::multimap<ReducedFraction, MidiChord>::iterator> &nonTuplets)
|
|
{
|
|
std::set<std::pair<const ReducedFraction, MidiChord> *> chords;
|
|
for (const auto &chord: nonTuplets) {
|
|
if (chords.find(&*chord) == chords.end())
|
|
chords.insert(&*chord);
|
|
else
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
size_t chordCount(const std::vector<TupletInfo> &tuplets,
|
|
const std::list<std::multimap<ReducedFraction, MidiChord>::iterator> &nonTuplets)
|
|
{
|
|
size_t sum = nonTuplets.size();
|
|
for (const auto &tuplet: tuplets) {
|
|
sum += tuplet.chords.size();
|
|
}
|
|
return sum;
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
// for the case !useMultipleVoices
|
|
|
|
void excludeExtraVoiceTuplets(
|
|
std::vector<TupletInfo> &tuplets,
|
|
std::list<std::multimap<ReducedFraction, MidiChord>::iterator> &nonTuplets,
|
|
const ReducedFraction ®ularRaster)
|
|
{
|
|
// remove overlapping tuplets
|
|
size_t sz = tuplets.size();
|
|
if (sz == 0)
|
|
return;
|
|
while (true) {
|
|
bool change = false;
|
|
for (size_t i = 0; i < sz - 1; ++i) {
|
|
const auto interval1 = tupletInterval(tuplets[i]);
|
|
for (size_t j = i + 1; j < sz; ++j) {
|
|
const auto interval2 = tupletInterval(tuplets[j]);
|
|
if (haveIntersection(interval1, interval2)) {
|
|
--sz;
|
|
if (j < sz)
|
|
tuplets[j] = tuplets[sz];
|
|
--sz;
|
|
if (i < sz)
|
|
tuplets[i] = tuplets[sz];
|
|
change = true;
|
|
break;
|
|
}
|
|
}
|
|
if (change)
|
|
break;
|
|
}
|
|
if (!change || sz == 0)
|
|
break;
|
|
}
|
|
|
|
if (sz > 0) { // remove tuplets that are overlapped with non-tuplets
|
|
const auto nonTupletIntervals = findNonTupletIntervals(nonTuplets, regularRaster);
|
|
|
|
for (size_t i = 0; i < sz; ) {
|
|
const auto interval = tupletInterval(tuplets[i]);
|
|
if (haveIntersection(interval, nonTupletIntervals)) {
|
|
for (const auto &chord: tuplets[i].chords)
|
|
nonTuplets.push_back(chord.second);
|
|
--sz;
|
|
if (i < sz) {
|
|
tuplets[i] = tuplets[sz];
|
|
continue;
|
|
}
|
|
}
|
|
++i;
|
|
}
|
|
}
|
|
|
|
Q_ASSERT_X(areAllElementsUnique(nonTuplets),
|
|
"MIDI tuplets: excludeExtraVoiceTuplets", "non unique chords in non-tuplets");
|
|
|
|
tuplets.resize(sz);
|
|
}
|
|
|
|
void assignVoices(std::vector<TupletInfo> &tuplets,
|
|
std::list<std::multimap<ReducedFraction, MidiChord>::iterator> &nonTuplets,
|
|
const std::vector<TiedTuplet> &backTiedTuplets,
|
|
const ReducedFraction &startBarTick,
|
|
const ReducedFraction ®ularRaster)
|
|
{
|
|
#ifdef QT_DEBUG
|
|
size_t oldChordCount = chordCount(tuplets, nonTuplets);
|
|
#endif
|
|
Q_ASSERT_X(!haveTupletsEmptyChords(tuplets),
|
|
"MIDI tuplets: assignVoices", "Empty tuplet chords");
|
|
|
|
auto pendingTuplets = findPendingTuplets(tuplets);
|
|
auto pendingNonTuplets = findPendingNonTuplets(nonTuplets);
|
|
const auto forTiedTuplets = findForTiedTuplets(tuplets, backTiedTuplets,
|
|
pendingNonTuplets, startBarTick);
|
|
std::map<int, std::vector<std::pair<ReducedFraction, ReducedFraction>>> tupletIntervals;
|
|
|
|
for (const auto &t: forTiedTuplets) {
|
|
const int i = t.first;
|
|
const int voice = t.second;
|
|
setTupletVoice(tuplets[i].chords, voice);
|
|
// remove tuplets with already set voices
|
|
pendingTuplets.erase(i);
|
|
tupletIntervals[voice].push_back(tupletInterval(tuplets[i]));
|
|
}
|
|
|
|
for (const TiedTuplet &tuplet: backTiedTuplets) {
|
|
setTupletVoice(tuplets[tuplet.tupletIndex].chords, tuplet.voice);
|
|
pendingTuplets.erase(tuplet.tupletIndex);
|
|
tupletIntervals[tuplet.voice].push_back(
|
|
tupletInterval(tuplets[tuplet.tupletIndex]));
|
|
// set for-tied chords
|
|
// some chords can be the same as in forTiedTuplets
|
|
|
|
// only for chords in the current bar (because of tol some can be outside)
|
|
if (tuplet.chord->first < startBarTick)
|
|
continue;
|
|
tuplet.chord->second.voice = tuplet.voice;
|
|
// remove chords with already set voices
|
|
pendingNonTuplets.erase(tuplet.chord);
|
|
}
|
|
|
|
{
|
|
setTupletVoices(tuplets, pendingTuplets, tupletIntervals);
|
|
|
|
Q_ASSERT_X((voiceLimit() == 1) ? pendingTuplets.empty() : true,
|
|
"MIDI tuplets: assignVoices", "Unused tuplets for the case !useMultipleVoices");
|
|
|
|
removeUnusedTuplets(tuplets, nonTuplets, pendingTuplets, pendingNonTuplets);
|
|
setNonTupletVoices(pendingNonTuplets, tupletIntervals, regularRaster);
|
|
}
|
|
|
|
Q_ASSERT_X(pendingNonTuplets.empty(),
|
|
"MIDI tuplets: assignVoices", "Unused non-tuplets");
|
|
Q_ASSERT_X(!haveTupletsEmptyChords(tuplets),
|
|
"MIDI tuplets: assignVoices", "Empty tuplet chords");
|
|
Q_ASSERT_X(doTupletChordsHaveSameVoice(tuplets),
|
|
"MIDI tuplets: assignVoices", "Tuplet chords have different voices");
|
|
Q_ASSERT_X(!haveOverlappingVoices(nonTuplets, tuplets, regularRaster, backTiedTuplets),
|
|
"MIDI tuplets: assignVoices", "Overlapping tuplets of the same voice");
|
|
#ifdef QT_DEBUG
|
|
size_t newChordCount = chordCount(tuplets, nonTuplets);
|
|
#endif
|
|
Q_ASSERT_X(oldChordCount == newChordCount,
|
|
"MIDI tuplets: assignVoices", "Chord count is not preserved");
|
|
}
|
|
|
|
std::vector<TupletData> convertToData(const std::vector<TupletInfo> &tuplets)
|
|
{
|
|
std::vector<TupletData> tupletsData;
|
|
for (const auto &tupletInfo: tuplets) {
|
|
MidiTuplet::TupletData tupletData = {tupletInfo.chords.begin()->second->second.voice,
|
|
tupletInfo.onTime,
|
|
tupletInfo.len,
|
|
tupletInfo.tupletNumber,
|
|
tupletInfo.tupletQuant,
|
|
{}};
|
|
tupletsData.push_back(tupletData);
|
|
}
|
|
return tupletsData;
|
|
}
|
|
|
|
// check is the chord already in tuplet in prev bar or division
|
|
// it's possible because we use (startDivTick - tol) as a start tick
|
|
|
|
template <typename Iter>
|
|
Iter findTupletFreeChord(const Iter &startChordIt,
|
|
const Iter &endChordIt,
|
|
const ReducedFraction &startDivTick)
|
|
{
|
|
auto result = startChordIt;
|
|
for (auto it = startChordIt; it != endChordIt && it->first < startDivTick; ++it) {
|
|
if (it->second.isInTuplet)
|
|
result = std::next(it);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void resetTupletVoices(std::vector<TupletInfo> &tuplets)
|
|
{
|
|
for (auto &tuplet: tuplets) {
|
|
for (auto &chord: tuplet.chords) {
|
|
MidiChord &midiChord = chord.second->second;
|
|
midiChord.voice = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// detect staccato notes; later sum length of rests of this notes
|
|
// will be reduced by enlarging the length of notes to the tuplet note length
|
|
|
|
void detectStaccato(TupletInfo &tuplet)
|
|
{
|
|
if ((int)tuplet.chords.size() >= tupletLimits(tuplet.tupletNumber).minNoteCountStaccato) {
|
|
const auto tupletNoteLen = tuplet.len / tuplet.tupletNumber;
|
|
for (auto &chord: tuplet.chords) {
|
|
MidiChord &midiChord = chord.second->second;
|
|
for (int i = 0; i != midiChord.notes.size(); ++i) {
|
|
if (midiChord.notes[i].offTime - chord.first < tupletNoteLen / 2) {
|
|
// later if chord have one or more notes
|
|
// with staccato -> entire chord is staccato
|
|
|
|
// don't mark note as staccato here, only remember it
|
|
// because different tuplets may contain this note,
|
|
// it will be resolved after tuplet filtering
|
|
tuplet.staccatoChords.insert({chord.first, i});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// this function is needed because if there are additional chords
|
|
// that can be in the middle between tuplet chords,
|
|
// and tuplet chords are staccato, i.e. have short length,
|
|
// then such a long tuplet with lots of short chords
|
|
// would be not pretty-looked converted to notation
|
|
|
|
template <typename Iter>
|
|
bool haveChordsInTheMiddleBetweenTupletChords(
|
|
const Iter &startDivChordIt,
|
|
const Iter &endDivChordIt,
|
|
const TupletInfo &tuplet)
|
|
{
|
|
const auto tupletNoteLen = tuplet.len / tuplet.tupletNumber;
|
|
for (auto it = startDivChordIt; it != endDivChordIt; ++it) {
|
|
for (int i = 0; i != tuplet.tupletNumber; ++i) {
|
|
const auto pos = tuplet.onTime + tupletNoteLen * i + tupletNoteLen / 2;
|
|
if ((pos - it->first).absValue() < tuplet.regularQuant)
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void findTupletQuantizedOffTime(std::vector<TupletInfo> &tuplets,
|
|
const ReducedFraction &barStart,
|
|
const ReducedFraction &barEnd)
|
|
{
|
|
for (auto &tuplet: tuplets) {
|
|
const auto tupletNoteLen = tuplet.len / tuplet.tupletNumber;
|
|
for (auto it = tuplet.chords.begin(); it != tuplet.chords.end(); ++it) {
|
|
MidiChord &midiChord = it->second->second;
|
|
for (auto ¬e: midiChord.notes) {
|
|
if (note.staccato) {
|
|
// decrease tuplet error by enlarging staccato notes:
|
|
// make note.len = tuplet note length
|
|
auto offTime = barStart + Quantize::quantizeValue(
|
|
it->first + tupletNoteLen - barStart,
|
|
tuplet.tupletQuant);
|
|
auto next = std::next(it);
|
|
if (next != tuplet.chords.end()) {
|
|
const auto nextOnTime = next->second->second.quantizedOnTime;
|
|
|
|
Q_ASSERT_X(nextOnTime != ReducedFraction(-1, 1),
|
|
"MidiTuplet::findTupletQuantizedOffTime",
|
|
"Tuplet onTime is not quantized but it should at this time");
|
|
|
|
if (offTime > nextOnTime)
|
|
offTime = nextOnTime;
|
|
}
|
|
note.quantizedOffTime = offTime;
|
|
}
|
|
else {
|
|
if (note.offTime <= tuplet.onTime + tuplet.len) {
|
|
note.quantizedOffTime = barStart + Quantize::quantizeValue(
|
|
note.offTime - barStart, tuplet.tupletQuant);
|
|
}
|
|
else if (note.offTime == ReducedFraction(-1, 1) // unset yet
|
|
&& note.offTime <= barEnd) { // belongs to this bar
|
|
note.quantizedOffTime = Quantize::quantizeValue(
|
|
note.offTime, tuplet.regularQuant);
|
|
}
|
|
// outside bar quantization cases will be considered later
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void markStaccatoTupletNotes(std::vector<TupletInfo> &tuplets)
|
|
{
|
|
for (auto &tuplet: tuplets) {
|
|
for (const auto &staccato: tuplet.staccatoChords) {
|
|
const auto it = tuplet.chords.find(staccato.first);
|
|
if (it != tuplet.chords.end()) {
|
|
MidiChord &midiChord = it->second->second;
|
|
midiChord.notes[staccato.second].staccato = true;
|
|
}
|
|
}
|
|
tuplet.staccatoChords.clear();
|
|
}
|
|
}
|
|
|
|
// if notes with staccato were in tuplets previously - remove staccato
|
|
|
|
void cleanStaccatoOfNonTuplets(
|
|
std::list<std::multimap<ReducedFraction, MidiChord>::iterator> &nonTuplets)
|
|
{
|
|
for (auto &nonTuplet: nonTuplets) {
|
|
for (auto ¬e: nonTuplet->second.notes) {
|
|
if (note.staccato)
|
|
note.staccato = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void findTupletQuantizedOnTime(std::vector<TupletInfo> &tuplets,
|
|
const ReducedFraction &startBarTick)
|
|
{
|
|
for (auto &tuplet: tuplets) {
|
|
for (auto &chord: tuplet.chords) {
|
|
if (chord.first < startBarTick) {
|
|
chord.second->second.quantizedOnTime = startBarTick;
|
|
}
|
|
else {
|
|
chord.second->second.quantizedOnTime = startBarTick + Quantize::quantizeValue(
|
|
chord.first - startBarTick, tuplet.tupletQuant);
|
|
}
|
|
|
|
Q_ASSERT_X(chord.second->second.quantizedOnTime >= tuplet.onTime,
|
|
"MidiTuplet::findTupletQuantizedOnTime",
|
|
"Chord onTime value is less than tuplet begin time");
|
|
}
|
|
}
|
|
}
|
|
|
|
void findNonTupletQuantizedOnTime(
|
|
std::list<std::multimap<ReducedFraction, MidiChord>::iterator> &nonTuplets,
|
|
const ReducedFraction ®ularRaster)
|
|
{
|
|
for (auto &nonTuplet: nonTuplets) {
|
|
nonTuplet->second.quantizedOnTime = Quantize::quantizeValue(
|
|
nonTuplet->first, regularRaster);
|
|
}
|
|
}
|
|
|
|
void findNonTupletQuantizedOffTime(
|
|
std::list<std::multimap<ReducedFraction, MidiChord>::iterator> &nonTuplets,
|
|
const ReducedFraction ®ularRaster,
|
|
const ReducedFraction &endBarTick)
|
|
{
|
|
for (auto &nonTuplet: nonTuplets) {
|
|
for (auto ¬e: nonTuplet->second.notes) {
|
|
// if offTime is already set or belongs to another bar - go to next note
|
|
if (note.quantizedOffTime == ReducedFraction(-1, 1)
|
|
&& note.offTime <= endBarTick) {
|
|
const auto raster = Quantize::reduceRasterIfDottedNote(
|
|
note.offTime - nonTuplet->first, regularRaster);
|
|
note.quantizedOffTime = Quantize::quantizeValue(note.offTime, raster);
|
|
}
|
|
// outside bar quantization cases will be considered later
|
|
}
|
|
}
|
|
}
|
|
|
|
void findTiedQuantizedOffTime(const std::vector<TiedTuplet> &backTiedTuplets,
|
|
const std::vector<TupletInfo> &tuplets,
|
|
const ReducedFraction &startBarTick)
|
|
{
|
|
for (const TiedTuplet &tuplet: backTiedTuplets) {
|
|
for (auto ¬e: tuplet.chord->second.notes) {
|
|
if (note.offTime > tuplets[tuplet.tupletIndex].onTime) {
|
|
|
|
Q_ASSERT_X(note.quantizedOffTime == ReducedFraction(-1, 1),
|
|
"MidiTuplet::findTiedQuantizedOffTime",
|
|
"Note quantized off time is already set");
|
|
|
|
note.quantizedOffTime = startBarTick + Quantize::quantizeValue(
|
|
note.offTime - startBarTick, tuplets[tuplet.tupletIndex].tupletQuant);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<TupletData> findTuplets(const ReducedFraction &startBarTick,
|
|
const ReducedFraction &endBarTick,
|
|
const ReducedFraction &barFraction,
|
|
std::multimap<ReducedFraction, MidiChord> &chords)
|
|
{
|
|
if (chords.empty() || startBarTick >= endBarTick) // invalid cases
|
|
return std::vector<TupletData>();
|
|
const auto operations = preferences.midiImportOperations.currentTrackOperations();
|
|
if (!operations.tuplets.doSearch)
|
|
return std::vector<TupletData>();
|
|
auto startBarChordIt = MChord::findFirstChordInRange(chords, startBarTick, endBarTick);
|
|
startBarChordIt = findTupletFreeChord(startBarChordIt, chords.end(), startBarTick);
|
|
if (startBarChordIt == chords.end()) // no chords in this bar
|
|
return std::vector<TupletData>();
|
|
|
|
const auto endBarChordIt = chords.lower_bound(endBarTick);
|
|
const auto regularRaster = Quantize::findRegularQuantRaster(startBarChordIt, endBarChordIt,
|
|
endBarTick);
|
|
const auto tol = regularRaster / 2;
|
|
// update start chord: use chords with onTime >= (start bar tick - tol)
|
|
startBarChordIt = MChord::findFirstChordInRange(chords, startBarTick - tol, endBarTick);
|
|
const auto divLengths = Meter::divisionsOfBarForTuplets(barFraction);
|
|
|
|
std::vector<TupletInfo> tuplets;
|
|
for (const auto &divLen: divLengths) {
|
|
const auto tupletNumbers = findTupletNumbers(divLen, barFraction);
|
|
const auto div = barFraction / divLen;
|
|
const int divCount = div.numerator() / div.denominator();
|
|
|
|
for (int i = 0; i != divCount; ++i) {
|
|
const auto startDivTime = startBarTick + divLen * i;
|
|
const auto endDivTime = startBarTick + divLen * (i + 1);
|
|
// check which chords can be inside tuplet period
|
|
// [startDivTime - tol, endDivTime]
|
|
auto startDivChordIt = MChord::findFirstChordInRange(startDivTime - tol, endDivTime,
|
|
startBarChordIt, endBarChordIt);
|
|
startDivChordIt = findTupletFreeChord(startDivChordIt, endBarChordIt, startDivTime);
|
|
if (startDivChordIt == endBarChordIt)
|
|
continue;
|
|
|
|
Q_ASSERT_X(startDivChordIt->second.isInTuplet == false,
|
|
"MIDI tuplets: findTuplets", "Voice of the chord has been already set");
|
|
|
|
// end iterator, as usual, will point to the next - invalid chord
|
|
const auto endDivChordIt = chords.lower_bound(endDivTime);
|
|
// try different tuplets, nested tuplets are not allowed
|
|
for (const auto &tupletNumber: tupletNumbers) {
|
|
const auto tupletNoteLen = divLen / tupletNumber;
|
|
if (tupletNoteLen < regularRaster)
|
|
continue;
|
|
auto tupletInfo = findTupletApproximation(divLen, tupletNumber,
|
|
regularRaster, startDivTime, startDivChordIt, endDivChordIt);
|
|
|
|
if (!haveChordsInTheMiddleBetweenTupletChords(
|
|
startDivChordIt, endDivChordIt, tupletInfo)) {
|
|
detectStaccato(tupletInfo);
|
|
}
|
|
tupletInfo.sumLengthOfRests = findSumLengthOfRests(tupletInfo, startBarTick);
|
|
|
|
if (!isTupletAllowed(tupletInfo))
|
|
continue;
|
|
tuplets.push_back(tupletInfo); // tuplet found
|
|
} // next tuplet type
|
|
}
|
|
}
|
|
|
|
filterTuplets(tuplets);
|
|
|
|
// later notes will be sorted and their indexes become invalid
|
|
// so assign staccato information to notes now
|
|
markStaccatoTupletNotes(tuplets);
|
|
|
|
// because of tol for non-tuplets we should use only chords with onTime >= bar start
|
|
auto startNonTupletChordIt = startBarChordIt;
|
|
while (startNonTupletChordIt->first < startBarTick)
|
|
++startNonTupletChordIt;
|
|
auto nonTuplets = findNonTupletChords(tuplets, startNonTupletChordIt, endBarChordIt);
|
|
if (tupletVoiceLimit() == 1)
|
|
excludeExtraVoiceTuplets(tuplets, nonTuplets, regularRaster);
|
|
|
|
resetTupletVoices(tuplets); // because of tol some chords may have non-zero voices
|
|
addChordsBetweenTupletNotes(tuplets, nonTuplets, startBarTick);
|
|
sortNotesByPitch(startBarChordIt, endBarChordIt);
|
|
sortTupletsByAveragePitch(tuplets);
|
|
|
|
if (operations.useMultipleVoices) {
|
|
splitFirstTupletChords(tuplets, chords);
|
|
minimizeOffTimeError(tuplets, chords, nonTuplets, startBarTick);
|
|
}
|
|
|
|
cleanStaccatoOfNonTuplets(nonTuplets);
|
|
|
|
Q_ASSERT_X(!doTupletsHaveCommonChords(tuplets),
|
|
"MIDI tuplets: findTuplets", "Tuplets have common chords but they shouldn't");
|
|
Q_ASSERT_X((voiceLimit() == 1)
|
|
? !haveOverlappingVoices(nonTuplets, tuplets, regularRaster)
|
|
: true,
|
|
"MIDI tuplets: findTuplets",
|
|
"Overlapping tuplet and non-tuplet voices for the case !useMultipleVoices");
|
|
|
|
const auto prevBarStart = findPrevBarStart(startBarTick, endBarTick - startBarTick);
|
|
const auto backTiedTuplets = findBackTiedTuplets(chords, tuplets, prevBarStart, startBarTick);
|
|
|
|
assignVoices(tuplets, nonTuplets, backTiedTuplets, startBarTick, regularRaster);
|
|
|
|
findTiedQuantizedOffTime(backTiedTuplets, tuplets, startBarTick);
|
|
findTupletQuantizedOnTime(tuplets, startBarTick);
|
|
findTupletQuantizedOffTime(tuplets, startBarTick, endBarTick);
|
|
findNonTupletQuantizedOnTime(nonTuplets, regularRaster);
|
|
findNonTupletQuantizedOffTime(nonTuplets, regularRaster, endBarTick);
|
|
|
|
return convertToData(tuplets);
|
|
}
|
|
|
|
std::multimap<ReducedFraction, TupletData>
|
|
findAllTuplets(std::multimap<ReducedFraction, MidiChord> &chords,
|
|
const TimeSigMap *sigmap,
|
|
const ReducedFraction &lastTick)
|
|
{
|
|
std::multimap<ReducedFraction, TupletData> tupletEvents;
|
|
ReducedFraction startBarTick = {0, 1};
|
|
|
|
for (int i = 1;; ++i) { // iterate over all measures by indexes
|
|
const auto endBarTick = ReducedFraction::fromTicks(sigmap->bar2tick(i, 0));
|
|
const auto barFraction = ReducedFraction(sigmap->timesig(startBarTick.ticks()).timesig());
|
|
const auto tuplets = findTuplets(startBarTick, endBarTick, barFraction, chords);
|
|
for (const auto &tupletData: tuplets)
|
|
tupletEvents.insert({tupletData.onTime, tupletData});
|
|
if (endBarTick > lastTick)
|
|
break;
|
|
startBarTick = endBarTick;
|
|
}
|
|
return tupletEvents;
|
|
}
|
|
|
|
// tuplets with no chords are removed
|
|
// tuplets with single chord with chord.onTime = tuplet.onTime
|
|
// and chord.len = tuplet.len are removed as well
|
|
|
|
void removeEmptyTuplets(MTrack &track)
|
|
{
|
|
if (track.tuplets.empty())
|
|
return;
|
|
for (auto it = track.tuplets.begin(); it != track.tuplets.end(); ) {
|
|
const auto &tupletData = it->second;
|
|
bool ok = false;
|
|
for (const auto &chord: track.chords) {
|
|
if (chord.first >= tupletData.onTime + tupletData.len)
|
|
break;
|
|
if (tupletData.voice != chord.second.voice)
|
|
continue;
|
|
const ReducedFraction &onTime = chord.first;
|
|
if (onTime >= tupletData.onTime
|
|
&& onTime < tupletData.onTime + tupletData.len) {
|
|
// tuplet contains at least one chord
|
|
// check now for notes with len == tupletData.len
|
|
if (onTime == tupletData.onTime) {
|
|
for (const auto ¬e: chord.second.notes) {
|
|
if (note.offTime - onTime < tupletData.len) {
|
|
ok = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
ok = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!ok) {
|
|
it = track.tuplets.erase(it);
|
|
continue;
|
|
}
|
|
++it;
|
|
}
|
|
}
|
|
|
|
void addElementToTuplet(int voice,
|
|
const ReducedFraction &onTime,
|
|
const ReducedFraction &len,
|
|
DurationElement *el,
|
|
std::multimap<ReducedFraction, TupletData> &tuplets)
|
|
{
|
|
const auto it = findTupletForTimeRange(voice, onTime, len, tuplets);
|
|
if (it != tuplets.end()) {
|
|
auto &tuplet = const_cast<TupletData &>(it->second);
|
|
tuplet.elements.push_back(el); // add chord/rest to the tuplet
|
|
}
|
|
}
|
|
|
|
void createTuplets(Staff *staff,
|
|
const std::multimap<ReducedFraction, TupletData> &tuplets)
|
|
{
|
|
Score* score = staff->score();
|
|
const int track = staff->idx() * VOICES;
|
|
|
|
for (const auto &tupletEvent: tuplets) {
|
|
const auto &tupletData = tupletEvent.second;
|
|
if (tupletData.elements.empty())
|
|
continue;
|
|
|
|
Tuplet* tuplet = new Tuplet(score);
|
|
const auto &tupletRatio = tupletLimits(tupletData.tupletNumber).ratio;
|
|
tuplet->setRatio(tupletRatio.fraction());
|
|
|
|
tuplet->setDuration(tupletData.len.fraction());
|
|
const TDuration baseLen = tupletData.len.fraction() / tupletRatio.denominator();
|
|
tuplet->setBaseLen(baseLen);
|
|
|
|
tuplet->setTrack(track);
|
|
tuplet->setTick(tupletData.onTime.ticks());
|
|
Measure* measure = score->tick2measure(tupletData.onTime.ticks());
|
|
tuplet->setParent(measure);
|
|
|
|
for (DurationElement *el: tupletData.elements) {
|
|
tuplet->add(el);
|
|
el->setTuplet(tuplet);
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace MidiTuplet
|
|
} // namespace Ms
|