MuseScore/mscore/importmidi_tuplet_detect.cpp
2014-07-29 18:38:06 +04:00

379 lines
17 KiB
C++

#include "importmidi_tuplet_detect.h"
#include "importmidi_tuplet.h"
#include "importmidi_meter.h"
#include "importmidi_chord.h"
#include "importmidi_quant.h"
#include "importmidi_inner.h"
#include "importmidi_operations.h"
#include "preferences.h"
#include <set>
namespace Ms {
namespace MidiTuplet {
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;
const auto tupletNoteLen = tupletInfo.len / num;
for (const auto &note: chordEventIt->second.notes) {
if ((note.offTime - chordEventIt->first - tupletNoteLen).absValue()
> tupletNoteLen / 2)
return false;
}
}
}
}
// for all tuplets
const auto &opers = preferences.midiImportOperations.data()->trackOpers;
const int minAllowedNoteCount = (opers.isHumanPerformance.value())
? tupletLimits(tupletInfo.tupletNumber).minNoteCountHuman
: tupletLimits(tupletInfo.tupletNumber).minNoteCount;
if ((int)tupletInfo.chords.size() < minAllowedNoteCount)
return false;
// allow duplets and quadruplets with error == regular error
// because tuplet notation is simpler in that case
if (tupletInfo.tupletNumber == 2 || tupletInfo.tupletNumber == 4) {
if (tupletInfo.tupletSumError > tupletInfo.regularSumError)
return false;
}
else {
if (tupletInfo.tupletSumError >= tupletInfo.regularSumError)
return false;
}
// at least one note has to have len >= (half tuplet note len)
const auto tupletNoteLen = tupletInfo.len / tupletInfo.tupletNumber;
for (const auto &tupletChord: tupletInfo.chords) {
for (const auto &note: 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 &opers = preferences.midiImportOperations.data()->trackOpers;
const int currentTrack = preferences.midiImportOperations.currentTrack();
std::vector<int> tupletNumbers;
if (Meter::isCompound(barFraction) && divLen == Meter::beatLength(barFraction)) {
if (opers.search2plets.value(currentTrack))
tupletNumbers.push_back(2);
if (opers.search4plets.value(currentTrack))
tupletNumbers.push_back(4);
}
else {
if (opers.search3plets.value(currentTrack))
tupletNumbers.push_back(3);
if (opers.search5plets.value(currentTrack))
tupletNumbers.push_back(5);
if (opers.search7plets.value(currentTrack))
tupletNumbers.push_back(7);
if (opers.search9plets.value(currentTrack))
tupletNumbers.push_back(9);
}
return tupletNumbers;
}
// 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};
const auto &opers = preferences.midiImportOperations.data()->trackOpers;
const int currentTrack = preferences.midiImportOperations.currentTrack();
for (const auto &chord: tupletInfo.chords) {
const auto staccatoIt = (opers.simplifyDurations.value(currentTrack))
? tupletInfo.staccatoChords.find(chord.first)
: tupletInfo.staccatoChords.end();
const MidiChord &midiChord = chord.second->second;
const auto &chordOnTime = (chord.second->first < startBarTick)
? startBarTick
: Quantize::findQuantizedTupletChordOnTime(*chord.second, tupletInfo.len,
tupletLimits(tupletInfo.tupletNumber).ratio, startBarTick);
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 = Quantize::findQuantizedTupletNoteOffTime(chord.first, maxOffTime, tupletInfo.len,
tupletLimits(tupletInfo.tupletNumber).ratio, startBarTick).first;
if (beg >= tupletEndTime)
break;
}
if (beg < tupletEndTime)
sumLen += (tupletEndTime - beg);
return sumLen;
}
TupletInfo findTupletApproximation(
const ReducedFraction &tupletLen,
int tupletNumber,
const ReducedFraction &basicQuant,
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;
const auto tupletNoteLen = tupletLen / tupletNumber;
struct Error
{
bool operator<(const Error &e) const
{
if (tupletError < e.tupletError)
return true;
if (tupletError > e.tupletError)
return false;
return (tupletRegularDiff < e.tupletRegularDiff);
}
ReducedFraction tupletError;
ReducedFraction tupletRegularDiff;
};
struct Candidate
{
int posIndex;
std::multimap<ReducedFraction, MidiChord>::iterator chord;
ReducedFraction regularError;
};
std::multimap<Error, Candidate> chordCandidates;
for (int posIndex = 0; posIndex != tupletNumber; ++posIndex) {
const auto tupletNotePos = startTupletTime + tupletNoteLen * posIndex;
for (auto it = startChordIt; it != endChordIt; ++it) {
if (it->first < tupletNotePos - tupletNoteLen / 2)
continue;
if (it->first > tupletNotePos + tupletNoteLen / 2)
break;
const auto tupletError = (it->first - tupletNotePos).absValue();
const auto regularError = Quantize::findOnTimeQuantError(*it, basicQuant);
const auto diff = tupletError - regularError;
chordCandidates.insert({{tupletError, diff}, {posIndex, it, regularError}});
}
}
std::set<std::pair<const ReducedFraction, MidiChord> *> usedChords;
std::set<int> usedPosIndexes;
ReducedFraction diffSum;
int firstChordIndex = 0;
for (const auto &candidate: chordCandidates) {
if (diffSum + candidate.first.tupletRegularDiff > ReducedFraction(0, 1))
break;
const Candidate &c = candidate.second;
if (usedPosIndexes.find(c.posIndex) != usedPosIndexes.end())
continue;
if (usedChords.find(&*c.chord) != usedChords.end())
continue;
usedChords.insert(&*c.chord);
usedPosIndexes.insert(c.posIndex);
diffSum += candidate.first.tupletRegularDiff;
tupletInfo.chords.insert({c.chord->first, c.chord});
tupletInfo.tupletSumError += candidate.first.tupletError;
tupletInfo.regularSumError += c.regularError;
// if chord was inserted to the beginning - remember its pos index
if (c.chord->first == tupletInfo.chords.begin()->first)
firstChordIndex = c.posIndex;
}
tupletInfo.firstChordIndex = firstChordIndex;
return tupletInfo;
}
// 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 tied staccato chords would be not pretty-looked converted to notation
bool haveChordsInTheMiddleBetweenTupletChords(
const std::multimap<ReducedFraction, MidiChord>::iterator startDivChordIt,
const std::multimap<ReducedFraction, MidiChord>::iterator endDivChordIt,
const TupletInfo &tuplet)
{
std::set<std::pair<const ReducedFraction, MidiChord> *> tupletChords;
for (const auto &chord: tuplet.chords)
tupletChords.insert(&*chord.second);
const auto tupletNoteLen = tuplet.len / tuplet.tupletNumber;
for (auto it = startDivChordIt; it != endDivChordIt; ++it) {
if (tupletChords.find(&*it) != tupletChords.end())
continue;
for (int i = 0; i != tuplet.tupletNumber; ++i) {
const auto pos = tuplet.onTime + tupletNoteLen * i + tupletNoteLen / 2;
if ((pos - it->first).absValue() < tupletNoteLen / 2)
return true;
}
}
return false;
}
bool isTupletLenAllowed(
const ReducedFraction &tupletLen,
int tupletNumber,
const std::multimap<ReducedFraction, MidiChord>::const_iterator beg,
const std::multimap<ReducedFraction, MidiChord>::const_iterator end,
const ReducedFraction &basicQuant)
{
const auto tupletNoteLen = tupletLen / tupletNumber;
const auto regularQuant = Quantize::findQuantForRange(beg, end, basicQuant);
return tupletNoteLen >= regularQuant;
}
// only one chord from the next bar can be used to search tuplets
// and this chord should be the last in the current bar
bool isNextBarOwnershipOk(
const std::multimap<ReducedFraction, MidiChord>::iterator &startIt,
const std::multimap<ReducedFraction, MidiChord>::iterator &endIt,
const std::multimap<ReducedFraction, MidiChord> &chords,
int barIndex)
{
if (endIt == chords.begin() || endIt == startIt)
return false;
if (std::prev(endIt)->second.barIndex == barIndex)
return true;
int nextBarCounter = 0;
for (auto it = std::prev(endIt); ; --it) {
if (it->second.barIndex > barIndex)
++nextBarCounter;
if (it == startIt)
break;
}
return nextBarCounter == 1;
}
std::vector<TupletInfo> detectTuplets(
const std::multimap<ReducedFraction, MidiChord>::iterator &startBarChordIt,
const std::multimap<ReducedFraction, MidiChord>::iterator &endBarChordIt,
const ReducedFraction &startBarTick,
const ReducedFraction &barFraction,
std::multimap<ReducedFraction, MidiChord> &chords,
const ReducedFraction &basicQuant,
int barIndex)
{
const auto divLengths = Meter::divisionsOfBarForTuplets(barFraction);
std::vector<TupletInfo> tuplets;
int id = 0;
const auto tol = basicQuant / 2;
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]
const auto startDivTimeWithTol = qMax(startBarTick, startDivTime - tol);
auto startDivChordIt = MChord::findFirstChordInRange(startDivTimeWithTol, endDivTime,
startBarChordIt, endBarChordIt);
if (startDivChordIt == endBarChordIt)
continue;
Q_ASSERT_X(!startDivChordIt->second.isInTuplet,
"MIDI tuplets: findTuplets", "Tuplet chord has been already used");
// end iterator, as usual, point to the next - invalid chord
auto endDivChordIt = chords.lower_bound(endDivTime);
if (!isNextBarOwnershipOk(startDivChordIt, endDivChordIt, chords, barIndex))
continue;
// try different tuplets, nested tuplets are not allowed
// here chords from next bar can be captured
// if their on time < next bar start
for (const auto &tupletNumber: tupletNumbers) {
if (!isTupletLenAllowed(divLen, tupletNumber, startDivChordIt, endDivChordIt,
basicQuant)) {
continue;
}
auto tupletInfo = findTupletApproximation(divLen, tupletNumber,
basicQuant, startDivTime, startDivChordIt, endDivChordIt);
const auto &opers = preferences.midiImportOperations.data()->trackOpers;
const int currentTrack = preferences.midiImportOperations.currentTrack();
if (opers.simplifyDurations.value(currentTrack)) {
if (!haveChordsInTheMiddleBetweenTupletChords(
startDivChordIt, endDivChordIt, tupletInfo)) {
detectStaccato(tupletInfo);
}
}
tupletInfo.sumLengthOfRests = findSumLengthOfRests(tupletInfo, startBarTick);
if (!isTupletAllowed(tupletInfo))
continue;
tupletInfo.id = id++;
tuplets.push_back(tupletInfo); // tuplet found
} // next tuplet type
}
}
return tuplets;
}
} // namespace MidiTuplet
} // namespace Ms