1031 lines
42 KiB
C++
1031 lines
42 KiB
C++
#include "importmidi_voice.h"
|
|
|
|
#include "importmidi_tuplet.h"
|
|
#include "importmidi_inner.h"
|
|
#include "importmidi_chord.h"
|
|
#include "importmidi_meter.h"
|
|
#include "importmidi_operations.h"
|
|
#include "libmscore/sig.h"
|
|
#include "libmscore/mscore.h"
|
|
#include "preferences.h"
|
|
#include "libmscore/durationtype.h"
|
|
|
|
|
|
namespace Ms {
|
|
namespace MidiVoice {
|
|
|
|
// no more than VOICES
|
|
|
|
int toIntVoiceCount(MidiOperations::VoiceCount value)
|
|
{
|
|
switch (value) {
|
|
case MidiOperations::VoiceCount::V_1:
|
|
return 1;
|
|
case MidiOperations::VoiceCount::V_2:
|
|
return 2;
|
|
case MidiOperations::VoiceCount::V_3:
|
|
return 3;
|
|
case MidiOperations::VoiceCount::V_4:
|
|
return 4;
|
|
}
|
|
return VOICES;
|
|
}
|
|
|
|
int voiceLimit()
|
|
{
|
|
const auto &opers = preferences.midiImportOperations.data()->trackOpers;
|
|
const int currentTrack = preferences.midiImportOperations.currentTrack();
|
|
const int allowedVoiceCount = toIntVoiceCount(opers.maxVoiceCount.value(currentTrack));
|
|
|
|
Q_ASSERT_X(allowedVoiceCount <= VOICES,
|
|
"MidiVoice::voiceLimit",
|
|
"Allowed voice count exceeds MuseScore voice limit");
|
|
|
|
return allowedVoiceCount;
|
|
}
|
|
|
|
|
|
#ifdef QT_DEBUG
|
|
|
|
bool areNotesSortedByOffTimeInAscOrder(
|
|
const QList<MidiNote>& notes,
|
|
const std::vector<int> &groupOfIndexes)
|
|
{
|
|
for (int i = 0; i != (int)groupOfIndexes.size() - 1; ++i) {
|
|
if (notes[groupOfIndexes[i]].offTime > notes[groupOfIndexes[i + 1]].offTime)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool areNotesSortedByOffTimeInAscOrder(const QList<MidiNote>& notes)
|
|
{
|
|
for (int i = 0; i != (int)notes.size() - 1; ++i) {
|
|
if (notes[i].offTime > notes[i].offTime)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool doesTupletAlreadyExist(
|
|
const ReducedFraction &tupletOnTime,
|
|
int voice,
|
|
const std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets)
|
|
{
|
|
const auto range = tuplets.equal_range(tupletOnTime);
|
|
bool found = false;
|
|
for (auto it = range.first; it != range.second; ++it) {
|
|
if (it->second.voice == voice) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
bool areVoicesSame(const std::multimap<ReducedFraction, MidiChord> &chords)
|
|
{
|
|
for (const auto &chord: chords) {
|
|
const MidiChord &c = chord.second;
|
|
if (c.isInTuplet && c.tuplet->second.voice != c.voice)
|
|
return false;
|
|
for (const auto ¬e: c.notes) {
|
|
if (note.isInTuplet && note.tuplet->second.voice != c.voice)
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
bool allNotesHaveEqualLength(const QList<MidiNote> ¬es)
|
|
{
|
|
const auto &offTime = notes[0].offTime;
|
|
for (int i = 1; i != notes.size(); ++i) {
|
|
if (notes[i].offTime != offTime)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int findDurationCountInGroup(
|
|
const ReducedFraction &chordOnTime,
|
|
const QList<MidiNote> ¬es,
|
|
int voice,
|
|
const std::vector<int> &groupOfIndexes,
|
|
const TimeSigMap *sigmap,
|
|
const std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets)
|
|
{
|
|
Q_ASSERT_X(areNotesSortedByOffTimeInAscOrder(notes, groupOfIndexes),
|
|
"MidiVoice::findDurationCountInGroup",
|
|
"Notes are not sorted by off time in ascending order");
|
|
|
|
const auto &opers = preferences.midiImportOperations.data()->trackOpers;
|
|
const int currentTrack = preferences.midiImportOperations.currentTrack();
|
|
const bool useDots = opers.useDots.value(currentTrack);
|
|
|
|
int count = 0;
|
|
auto onTime = chordOnTime;
|
|
auto onTimeBarStart = MidiBar::findBarStart(onTime, sigmap);
|
|
auto onTimeBarFraction = ReducedFraction(
|
|
sigmap->timesig(onTimeBarStart.ticks()).timesig());
|
|
|
|
for (int i: groupOfIndexes) {
|
|
const auto &offTime = notes[i].offTime;
|
|
if (offTime == onTime)
|
|
continue;
|
|
const auto offTimeBarStart = MidiBar::findBarStart(offTime, sigmap);
|
|
|
|
if (offTimeBarStart != onTimeBarStart) {
|
|
const auto offTimeBarFraction = ReducedFraction(
|
|
sigmap->timesig(offTimeBarStart.ticks()).timesig());
|
|
|
|
const auto tupletsForDuration = MidiTuplet::findTupletsInBarForDuration(
|
|
voice, onTimeBarStart, onTime, offTimeBarStart - onTime, tuplets);
|
|
|
|
// additional durations on measure boundary
|
|
const auto durations = Meter::toDurationList(
|
|
onTime - onTimeBarStart, offTimeBarStart - onTimeBarStart,
|
|
offTimeBarFraction, tupletsForDuration, Meter::DurationType::NOTE,
|
|
useDots, false);
|
|
|
|
count += MidiDuration::durationCount(durations);
|
|
|
|
onTime = offTimeBarStart;
|
|
onTimeBarStart = offTimeBarStart;
|
|
onTimeBarFraction = offTimeBarFraction;
|
|
}
|
|
|
|
const auto tupletsForDuration = MidiTuplet::findTupletsInBarForDuration(
|
|
voice, onTimeBarStart, onTime, offTime - onTime, tuplets);
|
|
|
|
const auto durations = Meter::toDurationList(
|
|
onTime - onTimeBarStart, offTime - onTimeBarStart, onTimeBarFraction,
|
|
tupletsForDuration, Meter::DurationType::NOTE, useDots, false);
|
|
|
|
count += MidiDuration::durationCount(durations);
|
|
|
|
onTime = offTime;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
// count of resulting durations in music notation
|
|
|
|
int findDurationCount(
|
|
const QList<MidiNote> ¬es,
|
|
int voice,
|
|
int splitPoint,
|
|
const ReducedFraction &chordOnTime,
|
|
const TimeSigMap *sigmap,
|
|
const std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets)
|
|
{
|
|
std::vector<int> lowGroup;
|
|
std::vector<int> highGroup;
|
|
|
|
for (int i = 0; i != splitPoint; ++i)
|
|
lowGroup.push_back(i);
|
|
for (int i = splitPoint; i != notes.size(); ++i)
|
|
highGroup.push_back(i);
|
|
|
|
std::sort(lowGroup.begin(), lowGroup.end(),
|
|
[&](int i1, int i2) { return notes[i1].offTime < notes[i2].offTime; });
|
|
std::sort(highGroup.begin(), highGroup.end(),
|
|
[&](int i1, int i2) { return notes[i1].offTime < notes[i2].offTime; });
|
|
|
|
return findDurationCountInGroup(chordOnTime, notes, voice, lowGroup, sigmap, tuplets)
|
|
+ findDurationCountInGroup(chordOnTime, notes, voice, highGroup, sigmap, tuplets);
|
|
}
|
|
|
|
int findOptimalSplitPoint(
|
|
const std::multimap<ReducedFraction, MidiChord>::iterator &chordIt,
|
|
const TimeSigMap *sigmap,
|
|
const std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets,
|
|
const std::multimap<ReducedFraction, MidiChord> &chords)
|
|
{
|
|
const auto ¬es = chordIt->second.notes;
|
|
|
|
Q_ASSERT_X(!notes.isEmpty(),
|
|
"MidiVoice::findOptimalSplitPoint", "Notes are empty");
|
|
Q_ASSERT_X(areNotesSortedByOffTimeInAscOrder(notes),
|
|
"MidiVoice::findOptimalSplitPoint",
|
|
"Notes are not sorted by length in ascending order");
|
|
|
|
int optSplit = -1;
|
|
|
|
if (!allNotesHaveEqualLength(notes)) {
|
|
int minNoteCount = std::numeric_limits<int>::max();
|
|
|
|
for (int splitPoint = 1; splitPoint != notes.size(); ++splitPoint) {
|
|
// optimization: don't split notes with equal durations
|
|
if (notes[splitPoint - 1].offTime == notes[splitPoint].offTime)
|
|
continue;
|
|
int noteCount = findDurationCount(notes, chordIt->second.voice, splitPoint,
|
|
chordIt->first, sigmap, tuplets);
|
|
if (noteCount < minNoteCount) {
|
|
minNoteCount = noteCount;
|
|
optSplit = splitPoint;
|
|
}
|
|
}
|
|
|
|
Q_ASSERT_X(optSplit != -1,
|
|
"MidiVoice::findOptimalSplitPoint", "Optimal split point was not defined");
|
|
}
|
|
else {
|
|
const auto offTime = notes.front().offTime;
|
|
for (auto it = std::next(chordIt); it != chords.end(); ++it) {
|
|
if (it->first >= offTime)
|
|
break;
|
|
if (it->second.voice != chordIt->second.voice)
|
|
continue;
|
|
if (it->first < offTime) {
|
|
optSplit = 0;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return optSplit;
|
|
}
|
|
|
|
// which part of chord notes, sorted by length - low note indexes or high note indexes
|
|
// - should be moved to another voice
|
|
|
|
enum class MovedVoiceGroup {
|
|
LOW,
|
|
HIGH
|
|
};
|
|
|
|
struct VoiceSplit {
|
|
MovedVoiceGroup group;
|
|
int voice = -1;
|
|
};
|
|
|
|
std::multimap<ReducedFraction,
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator>::const_iterator
|
|
findInsertedTuplet(const ReducedFraction &onTime,
|
|
int voice,
|
|
const std::multimap<ReducedFraction,
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> &insertedTuplets)
|
|
{
|
|
const auto range = insertedTuplets.equal_range(onTime);
|
|
for (auto it = range.first; it != range.second; ++it) {
|
|
if (it->second->second.voice == voice)
|
|
return it;
|
|
}
|
|
return insertedTuplets.end();
|
|
}
|
|
|
|
// if new chord intersected with tuplet that already was inserted
|
|
// due to some previous chord separation - then it is not an intersection:
|
|
// the new chord belongs to this tuplet
|
|
|
|
bool hasIntersectionWithTuplets(
|
|
int voice,
|
|
const ReducedFraction &onTime,
|
|
const ReducedFraction &offTime,
|
|
const std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets,
|
|
const std::multimap<ReducedFraction,
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> &insertedTuplets,
|
|
const ReducedFraction &tupletOnTime)
|
|
{
|
|
const auto foundTuplets = MidiTuplet::findTupletsForTimeRange(voice, onTime,
|
|
offTime - onTime, tuplets);
|
|
for (const auto tupletIt: foundTuplets) {
|
|
const auto ins = findInsertedTuplet(tupletIt->first, voice, insertedTuplets);
|
|
const bool belongsToInserted = (ins != insertedTuplets.end() && ins->first == tupletOnTime);
|
|
if (!belongsToInserted)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void addGroupSplits(
|
|
std::vector<VoiceSplit> &splits,
|
|
const ReducedFraction &maxChordLength,
|
|
const std::multimap<ReducedFraction, MidiChord> &chords,
|
|
const std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets,
|
|
const std::multimap<ReducedFraction,
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> &insertedTuplets,
|
|
const ReducedFraction &tupletOnTime,
|
|
const ReducedFraction &onTime,
|
|
const ReducedFraction &groupOffTime,
|
|
int origVoice,
|
|
MovedVoiceGroup groupType,
|
|
int maxOccupiedVoice)
|
|
{
|
|
const int limit = voiceLimit();
|
|
bool splitAdded = false;
|
|
|
|
for (int voice = 0; voice != limit; ++voice) {
|
|
if (voice == origVoice)
|
|
continue;
|
|
if (voice > maxOccupiedVoice && splitAdded)
|
|
break;
|
|
if (hasIntersectionWithTuplets(voice, onTime, groupOffTime,
|
|
tuplets, insertedTuplets, tupletOnTime))
|
|
continue;
|
|
const auto foundChords = MChord::findChordsForTimeRange(
|
|
voice, onTime, groupOffTime, chords, maxChordLength);
|
|
if (!foundChords.empty())
|
|
continue;
|
|
|
|
splitAdded = true;
|
|
VoiceSplit split;
|
|
split.group = groupType;
|
|
split.voice = voice;
|
|
splits.push_back(split);
|
|
}
|
|
}
|
|
|
|
ReducedFraction maximizeOffTime(const MidiNote ¬e, const ReducedFraction& offTime)
|
|
{
|
|
auto result = offTime;
|
|
if (note.offTime > offTime)
|
|
result = note.offTime;
|
|
if (note.isInTuplet) {
|
|
const auto &tuplet = note.tuplet->second;
|
|
if (tuplet.onTime + tuplet.len > result)
|
|
result = tuplet.onTime + tuplet.len;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::vector<VoiceSplit> findPossibleVoiceSplits(
|
|
int origVoice,
|
|
const std::multimap<ReducedFraction, MidiChord>::iterator &chordIt,
|
|
int splitPoint,
|
|
const std::multimap<ReducedFraction, MidiChord> &chords,
|
|
const std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets,
|
|
const std::multimap<ReducedFraction,
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> &insertedTuplets,
|
|
int maxOccupiedVoice)
|
|
{
|
|
std::vector<VoiceSplit> splits;
|
|
|
|
ReducedFraction onTime = chordIt->first;
|
|
ReducedFraction lowGroupOffTime(0, 1);
|
|
ReducedFraction highGroupOffTime(0, 1);
|
|
|
|
const auto ¬es = chordIt->second.notes;
|
|
for (int i = 0; i != splitPoint; ++i)
|
|
lowGroupOffTime = maximizeOffTime(notes[i], lowGroupOffTime);
|
|
for (int i = splitPoint; i != notes.size(); ++i)
|
|
highGroupOffTime = maximizeOffTime(notes[i], highGroupOffTime);
|
|
|
|
ReducedFraction tupletOnTime(-1, 1);
|
|
if (chordIt->second.isInTuplet) {
|
|
const auto &tuplet = chordIt->second.tuplet->second;
|
|
tupletOnTime = tuplet.onTime;
|
|
if (tuplet.onTime < onTime)
|
|
onTime = tuplet.onTime;
|
|
if (tuplet.onTime + tuplet.len > lowGroupOffTime)
|
|
lowGroupOffTime = tuplet.onTime + tuplet.len;
|
|
if (tuplet.onTime + tuplet.len > highGroupOffTime)
|
|
highGroupOffTime = tuplet.onTime + tuplet.len;
|
|
}
|
|
|
|
const ReducedFraction maxChordLength = MChord::findMaxChordLength(chords);
|
|
|
|
if (splitPoint > 0) {
|
|
addGroupSplits(splits, maxChordLength, chords, tuplets, insertedTuplets,
|
|
tupletOnTime, onTime, lowGroupOffTime, origVoice,
|
|
MovedVoiceGroup::LOW, maxOccupiedVoice);
|
|
}
|
|
if (splitPoint < notes.size()) {
|
|
addGroupSplits(splits, maxChordLength, chords, tuplets, insertedTuplets,
|
|
tupletOnTime, onTime, highGroupOffTime, origVoice,
|
|
MovedVoiceGroup::HIGH, maxOccupiedVoice);
|
|
}
|
|
|
|
return splits;
|
|
}
|
|
|
|
const int MAX_PITCH_DIST = 1000;
|
|
|
|
int findPrevPitchDist(
|
|
const std::multimap<ReducedFraction, MidiChord>::iterator &chordIt,
|
|
int averagePitch,
|
|
const std::multimap<ReducedFraction, MidiChord> &chords,
|
|
int voice)
|
|
{
|
|
auto it = chordIt;
|
|
while (it != chords.begin()) {
|
|
--it;
|
|
if (it->second.voice == voice) {
|
|
return qAbs(MChord::chordAveragePitch(it->second.notes) - averagePitch);
|
|
}
|
|
}
|
|
return MAX_PITCH_DIST;
|
|
}
|
|
|
|
int findNextPitchDist(
|
|
const std::multimap<ReducedFraction, MidiChord>::iterator &chordIt,
|
|
int averagePitch,
|
|
const std::multimap<ReducedFraction, MidiChord> &chords,
|
|
int voice)
|
|
{
|
|
auto it = (chordIt == chords.end()) ? chordIt : std::next(chordIt);
|
|
while (it != chords.end()) {
|
|
if (it->second.voice == voice) {
|
|
return qAbs(MChord::chordAveragePitch(it->second.notes) - averagePitch);
|
|
}
|
|
++it;
|
|
}
|
|
return MAX_PITCH_DIST;
|
|
}
|
|
|
|
int findMinPitchDist(
|
|
int averagePitch,
|
|
const int voice,
|
|
const std::multimap<ReducedFraction, MidiChord>::iterator &chordIt,
|
|
const std::multimap<ReducedFraction, MidiChord> &chords)
|
|
{
|
|
const int OCTAVE = 12;
|
|
const int prevPitchDist = findPrevPitchDist(chordIt, averagePitch, chords, voice);
|
|
const int nextPitchDist = findNextPitchDist(chordIt, averagePitch, chords, voice);
|
|
|
|
int pitchDist = MAX_PITCH_DIST;
|
|
|
|
if (prevPitchDist < nextPitchDist && prevPitchDist <= OCTAVE)
|
|
pitchDist = prevPitchDist;
|
|
else if (nextPitchDist <= prevPitchDist && nextPitchDist <= OCTAVE)
|
|
pitchDist = nextPitchDist;
|
|
|
|
return pitchDist;
|
|
}
|
|
|
|
int findAverageLowPitch(const QList<MidiNote> ¬es, int splitPoint)
|
|
{
|
|
int averageLowPitch = 0;
|
|
for (int j = 0; j != splitPoint; ++j)
|
|
averageLowPitch += notes[j].pitch;
|
|
averageLowPitch = qRound(averageLowPitch * 1.0 / splitPoint);
|
|
|
|
return averageLowPitch;
|
|
}
|
|
|
|
int findAverageHighPitch(const QList<MidiNote> ¬es, int splitPoint)
|
|
{
|
|
int averageHighPitch = 0;
|
|
for (int j = splitPoint; j != notes.size(); ++j)
|
|
averageHighPitch += notes[j].pitch;
|
|
averageHighPitch = qRound(averageHighPitch * 1.0 / (notes.size() - splitPoint));
|
|
|
|
return averageHighPitch;
|
|
}
|
|
|
|
VoiceSplit findBestSplit(
|
|
const std::multimap<ReducedFraction, MidiChord>::iterator &chordIt,
|
|
const std::multimap<ReducedFraction, MidiChord> &chords,
|
|
const std::vector<VoiceSplit> &possibleSplits,
|
|
int splitPoint)
|
|
{
|
|
// to minimize <pitch distance, voice>
|
|
std::pair<int, int> minError{std::numeric_limits<int>::max(),
|
|
std::numeric_limits<int>::max()};
|
|
int bestSplit = -1;
|
|
|
|
for (int i = 0; i != (int)possibleSplits.size(); ++i) {
|
|
const int voice = possibleSplits[i].voice;
|
|
const auto ¬es = chordIt->second.notes;
|
|
int totalPitchDist = 0;
|
|
|
|
if (splitPoint > 0) {
|
|
const int averageLowPitch = findAverageLowPitch(notes, splitPoint);
|
|
const int lowVoice = (possibleSplits[i].group == MovedVoiceGroup::LOW)
|
|
? voice : chordIt->second.voice;
|
|
totalPitchDist += findMinPitchDist(averageLowPitch, lowVoice, chordIt, chords);
|
|
}
|
|
|
|
if (splitPoint < notes.size()) {
|
|
const int averageHighPitch = findAverageHighPitch(notes, splitPoint);
|
|
const int highVoice = (possibleSplits[i].group == MovedVoiceGroup::HIGH)
|
|
? voice : chordIt->second.voice;
|
|
totalPitchDist += findMinPitchDist(averageHighPitch, highVoice, chordIt, chords);
|
|
}
|
|
|
|
const std::pair<int, int> error{totalPitchDist, voice};
|
|
|
|
if (error < minError) {
|
|
minError = error;
|
|
bestSplit = i;
|
|
}
|
|
}
|
|
|
|
Q_ASSERT_X(bestSplit != -1, "MidiVoice::findBestSplit", "Best split was not found");
|
|
|
|
return possibleSplits[bestSplit];
|
|
}
|
|
|
|
void insertNewTuplet(
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator &tuplet,
|
|
const ReducedFraction &tupletOnTime,
|
|
int newVoice,
|
|
std::multimap<ReducedFraction, MidiChord> &chords,
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets,
|
|
std::multimap<ReducedFraction,
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> &insertedTuplets)
|
|
{
|
|
MidiTuplet::TupletData newTuplet = tuplet->second;
|
|
newTuplet.voice = newVoice;
|
|
|
|
Q_ASSERT_X(!doesTupletAlreadyExist(newTuplet.onTime, newVoice, tuplets),
|
|
"MidiVoice::addOrUpdateTuplet", "Tuplet already exists");
|
|
|
|
tuplet = tuplets.insert({tupletOnTime, newTuplet});
|
|
insertedTuplets.insert({tupletOnTime, tuplet});
|
|
// maybe impossible due to intersection check but check anyway:
|
|
// if there is a non-tuplet chord with on time = tuplet on time
|
|
// then add that chord to the new tuplet
|
|
const auto range = chords.equal_range(tupletOnTime);
|
|
for (auto it = range.first; it != range.second; ++it) {
|
|
MidiChord &chord = it->second;
|
|
if (chord.voice == newVoice && !chord.isInTuplet) {
|
|
chord.isInTuplet = true;
|
|
chord.tuplet = tuplet;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool canSplitTuplet(
|
|
const std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator &tuplet,
|
|
int newVoice,
|
|
const ReducedFraction &chordOnTime,
|
|
const QList<MidiNote> ¬es,
|
|
const std::multimap<ReducedFraction,
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> &insertedTuplets,
|
|
const std::multimap<ReducedFraction, MidiChord> &chords,
|
|
const ReducedFraction &maxChordLength)
|
|
{
|
|
const auto tupletOnTime = tuplet->first;
|
|
const auto &t = tuplet->second;
|
|
const auto insertedTuplet = findInsertedTuplet(tupletOnTime, newVoice, insertedTuplets);
|
|
|
|
bool needInsertTuplet = false;
|
|
if (insertedTuplet == insertedTuplets.end()
|
|
&& MidiTuplet::hasNonTrivialChord(chordOnTime, notes, t.onTime, t.len)) {
|
|
needInsertTuplet = true;
|
|
}
|
|
|
|
const bool needDeleteOldTuplet = MidiTuplet::isTupletUseless(t.voice, tupletOnTime,
|
|
t.len, maxChordLength, chords);
|
|
// insert new tuplet only if old tuplet should be deleted
|
|
// because parallel equal tuplets with different voices aren't pretty
|
|
if (needInsertTuplet && !needDeleteOldTuplet) {
|
|
// need to amend chord split - need but cannot insert tuplet,
|
|
// in that case no changes should be done earlier in this function!
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void splitTuplet(
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator &tuplet,
|
|
int newVoice,
|
|
const ReducedFraction &chordOnTime,
|
|
const QList<MidiNote> ¬es,
|
|
bool &isInTuplet,
|
|
std::multimap<ReducedFraction,
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> &insertedTuplets,
|
|
std::multimap<ReducedFraction, MidiChord> &chords,
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets,
|
|
const ReducedFraction &maxChordLength,
|
|
bool allowParallelTuplets)
|
|
{
|
|
|
|
Q_ASSERT_X(isInTuplet, "MidiVoice::splitTuplet",
|
|
"Tuplet chord/note is not actually in tuplet");
|
|
|
|
const auto oldTuplet = tuplet;
|
|
const auto tupletOnTime = tuplet->first;
|
|
const auto insertedTuplet = findInsertedTuplet(tupletOnTime, newVoice, insertedTuplets);
|
|
|
|
bool needInsertTuplet = false;
|
|
|
|
if (insertedTuplet == insertedTuplets.end()) {
|
|
const auto &t = tuplet->second;
|
|
if (MidiTuplet::hasNonTrivialChord(chordOnTime, notes, t.onTime, t.len))
|
|
needInsertTuplet = true;
|
|
else
|
|
isInTuplet = false;
|
|
}
|
|
else {
|
|
tuplet = insertedTuplet->second;
|
|
}
|
|
|
|
const auto &t = oldTuplet->second;
|
|
const bool needDeleteOldTuplet = MidiTuplet::isTupletUseless(
|
|
t.voice, t.onTime, t.len, maxChordLength, chords);
|
|
// insert new tuplet only if old tuplet was erased
|
|
// because parallel equal tuplets with different voices aren't pretty
|
|
const bool canInsertTuplet = (allowParallelTuplets)
|
|
? needInsertTuplet : (needInsertTuplet && needDeleteOldTuplet);
|
|
if (canInsertTuplet) {
|
|
insertNewTuplet(tuplet, tupletOnTime, newVoice, chords, tuplets, insertedTuplets);
|
|
isInTuplet = true;
|
|
}
|
|
if (needDeleteOldTuplet) // delete after insert, because oldTuplet can be used
|
|
MidiTuplet::removeTuplet(oldTuplet, tuplets, maxChordLength, chords);
|
|
|
|
Q_ASSERT_X(allowParallelTuplets || !needInsertTuplet || needDeleteOldTuplet,
|
|
"MidiVoice::splitTuplet",
|
|
"Tuplet need to be added but the old tuplet was not deleted");
|
|
}
|
|
|
|
bool updateChordTuplets(
|
|
MidiChord &chord,
|
|
const ReducedFraction &onTime,
|
|
std::multimap<ReducedFraction,
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> &insertedTuplets,
|
|
std::multimap<ReducedFraction, MidiChord> &chords,
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets,
|
|
const ReducedFraction &maxChordLength,
|
|
bool allowParallelTuplets)
|
|
{
|
|
if (!allowParallelTuplets) {
|
|
bool canDoSplit = true;
|
|
if (chord.isInTuplet && !canSplitTuplet(chord.tuplet, chord.voice, onTime,
|
|
chord.notes, insertedTuplets, chords,
|
|
maxChordLength)) {
|
|
canDoSplit = false;
|
|
}
|
|
if (canDoSplit) {
|
|
for (auto ¬e: chord.notes) {
|
|
if (note.isInTuplet && !canSplitTuplet(note.tuplet, chord.voice, onTime,
|
|
{note}, insertedTuplets, chords,
|
|
maxChordLength)) {
|
|
canDoSplit = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!canDoSplit)
|
|
return false;
|
|
}
|
|
|
|
if (chord.isInTuplet) {
|
|
splitTuplet(chord.tuplet, chord.voice, onTime,
|
|
chord.notes, chord.isInTuplet,
|
|
insertedTuplets, chords,
|
|
tuplets, maxChordLength, allowParallelTuplets);
|
|
}
|
|
for (auto ¬e: chord.notes) {
|
|
if (note.isInTuplet) {
|
|
splitTuplet(note.tuplet, chord.voice, onTime,
|
|
{note}, note.isInTuplet,
|
|
insertedTuplets, chords,
|
|
tuplets, maxChordLength, allowParallelTuplets);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
int findMaxOccupiedVoiceInBar(
|
|
const std::multimap<ReducedFraction, MidiChord>::iterator &chordIt,
|
|
const std::multimap<ReducedFraction, MidiChord> &chords)
|
|
{
|
|
const int barIndex = chordIt->second.barIndex;
|
|
int maxVoice = 0;
|
|
// look forward
|
|
for (auto it = chordIt; it != chords.end(); ++it) {
|
|
const MidiChord &chord = it->second;
|
|
if (chord.barIndex > barIndex + 1)
|
|
break;
|
|
if (chord.barIndex == barIndex && chord.voice > maxVoice)
|
|
maxVoice = chord.voice;
|
|
}
|
|
// look backward
|
|
for (auto it = chordIt; ; ) {
|
|
const MidiChord &chord = it->second;
|
|
if (chord.barIndex < barIndex - 1)
|
|
break;
|
|
if (chord.barIndex == barIndex && chord.voice > maxVoice)
|
|
maxVoice = chord.voice;
|
|
if (it == chords.begin())
|
|
break;
|
|
--it;
|
|
}
|
|
|
|
return maxVoice;
|
|
}
|
|
|
|
bool splitChordToVoice(
|
|
std::multimap<ReducedFraction, MidiChord>::iterator &chordIt,
|
|
const QSet<int> ¬esToMove,
|
|
int newVoice,
|
|
std::multimap<ReducedFraction, MidiChord> &chords,
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets,
|
|
std::multimap<ReducedFraction,
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> &insertedTuplets,
|
|
const ReducedFraction &maxChordLength,
|
|
bool allowParallelTuplets)
|
|
{
|
|
if (notesToMove.isEmpty())
|
|
return false;
|
|
|
|
bool splitDone = true;
|
|
const ReducedFraction onTime = chordIt->first;
|
|
MidiChord &chord = chordIt->second;
|
|
auto ¬es = chord.notes;
|
|
|
|
if (notesToMove.size() == notes.size()) {
|
|
// don't split chord, just move it to another voice
|
|
const int oldVoice = chord.voice; // remember for possible undo
|
|
chord.voice = newVoice;
|
|
|
|
const bool success = updateChordTuplets(chord, onTime, insertedTuplets,
|
|
chords, tuplets, maxChordLength,
|
|
allowParallelTuplets);
|
|
if (!success) {
|
|
splitDone = false;
|
|
chord.voice = oldVoice; // rollback
|
|
}
|
|
}
|
|
else { // split chord
|
|
MidiChord newChord(chord);
|
|
newChord.notes.clear();
|
|
newChord.voice = newVoice;
|
|
QList<MidiNote> updatedOldNotes;
|
|
|
|
for (int i = 0; i != notes.size(); ++i) {
|
|
if (notesToMove.contains(i))
|
|
newChord.notes.append(notes[i]);
|
|
else
|
|
updatedOldNotes.append(notes[i]);
|
|
}
|
|
|
|
const auto rememberedNotes = notes; // to undo split if necessary
|
|
// update notes before tuplet update because there will be check
|
|
// for empty tuplets
|
|
notes = updatedOldNotes;
|
|
|
|
const bool success = updateChordTuplets(newChord, onTime, insertedTuplets,
|
|
chords, tuplets, maxChordLength,
|
|
allowParallelTuplets);
|
|
if (success) {
|
|
|
|
Q_ASSERT_X(!notes.isEmpty(),
|
|
"MidiVoice::splitChordToVoice", "Old chord notes are empty");
|
|
Q_ASSERT_X(!newChord.notes.isEmpty(),
|
|
"MidiVoice::splitChordToVoice", "New chord notes are empty");
|
|
|
|
chordIt = chords.insert({onTime, newChord});
|
|
}
|
|
else {
|
|
notes = rememberedNotes; // rollback
|
|
splitDone = false;
|
|
}
|
|
}
|
|
return splitDone;
|
|
}
|
|
|
|
QSet<int> findNotesToMove(const QList<MidiNote> ¬es,
|
|
int splitPoint,
|
|
MovedVoiceGroup splitGroup)
|
|
{
|
|
QSet<int> notesToMove;
|
|
if (splitPoint == 0 || splitPoint == notes.size()) {
|
|
// don't split chord, just move it to another voice
|
|
for (int i = 0; i != notes.size(); ++i)
|
|
notesToMove.insert(i);
|
|
}
|
|
else {
|
|
switch (splitGroup) {
|
|
case MovedVoiceGroup::LOW:
|
|
for (int i = 0; i != splitPoint; ++i)
|
|
notesToMove.insert(i);
|
|
break;
|
|
case MovedVoiceGroup::HIGH:
|
|
for (int i = splitPoint; i != notes.size(); ++i)
|
|
notesToMove.insert(i);
|
|
break;
|
|
}
|
|
}
|
|
return notesToMove;
|
|
}
|
|
|
|
bool doVoiceSeparation(
|
|
std::multimap<ReducedFraction, MidiChord> &chords,
|
|
const TimeSigMap *sigmap,
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData> &tuplets)
|
|
{
|
|
|
|
Q_ASSERT_X(MidiTuplet::areTupletRangesOk(chords, tuplets),
|
|
"MidiVoice::doVoiceSeparation",
|
|
"Tuplet chord/note is outside tuplet "
|
|
"or non-tuplet chord/note is inside tuplet before voice separation");
|
|
|
|
MChord::sortNotesByLength(chords);
|
|
std::multimap<ReducedFraction,
|
|
std::multimap<ReducedFraction, MidiTuplet::TupletData>::iterator> insertedTuplets;
|
|
const ReducedFraction maxChordLength = MChord::findMaxChordLength(chords);
|
|
|
|
bool changed = false;
|
|
std::map<int, int> maxOccupiedVoices; // <bar index, max occupied voice>
|
|
|
|
for (auto it = chords.begin(); it != chords.end(); ++it) {
|
|
MidiChord &chord = it->second;
|
|
auto ¬es = chord.notes;
|
|
|
|
const int splitPoint = findOptimalSplitPoint(it, sigmap, tuplets, chords);
|
|
if (splitPoint == -1)
|
|
continue;
|
|
|
|
auto maxVoiceIt = maxOccupiedVoices.find(chord.barIndex);
|
|
if (maxVoiceIt == maxOccupiedVoices.end()) {
|
|
const int maxVoice = findMaxOccupiedVoiceInBar(it, chords);
|
|
maxVoiceIt = maxOccupiedVoices.insert({chord.barIndex, maxVoice}).first;
|
|
}
|
|
const auto possibleSplits = findPossibleVoiceSplits(
|
|
chord.voice, it, splitPoint, chords,
|
|
tuplets, insertedTuplets, maxVoiceIt->second);
|
|
if (possibleSplits.empty())
|
|
continue;
|
|
|
|
const VoiceSplit bestSplit = findBestSplit(it, chords, possibleSplits, splitPoint);
|
|
changed = true;
|
|
|
|
const QSet<int> notesToMove = findNotesToMove(notes, splitPoint, bestSplit.group);
|
|
const bool splitDone = splitChordToVoice(
|
|
it, notesToMove, bestSplit.voice, chords, tuplets,
|
|
insertedTuplets, maxChordLength);
|
|
if (splitDone && bestSplit.voice > maxVoiceIt->second)
|
|
maxVoiceIt->second = bestSplit.voice;
|
|
}
|
|
|
|
Q_ASSERT_X(MidiTuplet::areTupletRangesOk(chords, tuplets),
|
|
"MidiVoice::doVoiceSeparation",
|
|
"Tuplet chord/note is outside tuplet "
|
|
"or non-tuplet chord/note is inside tuplet after voice separation");
|
|
|
|
return changed;
|
|
}
|
|
|
|
int findBarIndexForOffTime(const ReducedFraction &offTime, const TimeSigMap *sigmap)
|
|
{
|
|
int barIndex, beat, tick;
|
|
sigmap->tickValues(offTime.ticks(), &barIndex, &beat, &tick);
|
|
if (beat == 0 && tick == 0)
|
|
--barIndex;
|
|
return barIndex;
|
|
}
|
|
|
|
int averagePitchOfChords(
|
|
const std::vector<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.notes;
|
|
for (const auto &midiNote: midiNotes) {
|
|
sumPitch += midiNote.pitch;
|
|
++noteCounter;
|
|
}
|
|
}
|
|
|
|
return qRound(sumPitch * 1.0 / noteCounter);
|
|
}
|
|
|
|
void sortVoicesByPitch(const std::map<int, std::vector<
|
|
std::multimap<ReducedFraction, MidiChord>::iterator>> &voiceChords)
|
|
{
|
|
// [newVoice] = <average pitch, old voice>
|
|
std::vector<std::pair<int, int>> pitchVoices;
|
|
for (const auto &v: voiceChords)
|
|
pitchVoices.push_back({averagePitchOfChords(v.second), v.first});
|
|
|
|
struct {
|
|
bool operator()(const std::pair<int, int> &p1,
|
|
const std::pair<int, int> &p2) const
|
|
{
|
|
if (p1.first > p2.first)
|
|
return true;
|
|
else if (p1.first < p2.first)
|
|
return false;
|
|
else
|
|
return p1.second < p2.second;
|
|
}
|
|
} comparator;
|
|
std::sort(pitchVoices.begin(), pitchVoices.end(), comparator);
|
|
|
|
for (int newVoice = 0; newVoice != (int)pitchVoices.size(); ++newVoice) {
|
|
const int oldVoice = pitchVoices[newVoice].second;
|
|
if (newVoice == oldVoice)
|
|
continue;
|
|
const auto it = voiceChords.find(oldVoice);
|
|
|
|
Q_ASSERT_X(it != voiceChords.end(),
|
|
"MidiVoice::sortVoicesByPitch", "Old voice not found");
|
|
|
|
for (auto &chord: it->second) {
|
|
MidiChord &c = chord->second;
|
|
c.voice = newVoice;
|
|
if (c.isInTuplet)
|
|
c.tuplet->second.voice = newVoice;
|
|
for (auto ¬e: c.notes) {
|
|
if (note.isInTuplet)
|
|
note.tuplet->second.voice = newVoice;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void sortVoices(
|
|
std::multimap<ReducedFraction, MidiChord> &chords,
|
|
const TimeSigMap *sigmap)
|
|
{
|
|
// <voice, chords>
|
|
std::map<int, std::vector<std::multimap<ReducedFraction, MidiChord>::iterator>> voiceChords;
|
|
int maxBarIndex = 0;
|
|
|
|
for (auto it = chords.begin(); it != chords.end(); ++it) {
|
|
const auto &chord = it->second;
|
|
|
|
// some notes: if chord off time belongs to tuplet
|
|
// then this tuplet belongs to the same bar as the chord off time;
|
|
// same is for chord on time
|
|
|
|
Q_ASSERT_X(chord.barIndex != -1,
|
|
"MidiVoice::sortVoices", "Chord bar index is undefined");
|
|
Q_ASSERT_X((!voiceChords.empty()) ? chord.barIndex <= maxBarIndex : true,
|
|
"MidiVoice::sortVoices", "Chord bar index is greater than current index");
|
|
|
|
voiceChords[chord.voice].push_back(it);
|
|
const int barIndex = findBarIndexForOffTime(
|
|
MChord::maxNoteOffTime(chord.notes), sigmap);
|
|
if (barIndex > maxBarIndex)
|
|
maxBarIndex = barIndex;
|
|
|
|
if (std::next(it) == chords.end() || std::next(it)->second.barIndex > maxBarIndex) {
|
|
sortVoicesByPitch(voiceChords);
|
|
voiceChords.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool separateVoices(std::multimap<int, MTrack> &tracks, const TimeSigMap *sigmap)
|
|
{
|
|
auto &opers = preferences.midiImportOperations;
|
|
bool changed = false;
|
|
|
|
for (auto &track: tracks) {
|
|
MTrack &mtrack = track.second;
|
|
if (mtrack.mtrack->drumTrack())
|
|
continue;
|
|
auto &chords = track.second.chords;
|
|
if (chords.empty())
|
|
continue;
|
|
const int userVoiceCount = toIntVoiceCount(
|
|
opers.data()->trackOpers.maxVoiceCount.value(mtrack.indexOfOperation));
|
|
// pass current track index through MidiImportOperations
|
|
// for further usage
|
|
MidiOperations::CurrentTrackSetter setCurrentTrack{opers, mtrack.indexOfOperation};
|
|
|
|
if (userVoiceCount > 1 && userVoiceCount <= voiceLimit()) {
|
|
|
|
Q_ASSERT_X(MidiTuplet::areAllTupletsReferenced(mtrack.chords, mtrack.tuplets),
|
|
"MidiVoice::separateVoices",
|
|
"Not all tuplets are referenced in chords or notes "
|
|
"before voice separation");
|
|
Q_ASSERT_X(areVoicesSame(mtrack.chords),
|
|
"MidiVoice::separateVoices", "Different voices of chord and tuplet "
|
|
"before voice separation");
|
|
|
|
if (!changed && doVoiceSeparation(mtrack.chords, sigmap, mtrack.tuplets))
|
|
changed = true;
|
|
|
|
Q_ASSERT_X(MidiTuplet::areAllTupletsReferenced(mtrack.chords, mtrack.tuplets),
|
|
"MidiVoice::separateVoices",
|
|
"Not all tuplets are referenced in chords or notes "
|
|
"after voice separation, before voice sort");
|
|
Q_ASSERT_X(areVoicesSame(mtrack.chords),
|
|
"MidiVoice::separateVoices", "Different voices of chord and tuplet "
|
|
"after voice separation, before voice sort");
|
|
|
|
sortVoices(mtrack.chords, sigmap);
|
|
|
|
Q_ASSERT_X(MidiTuplet::areAllTupletsReferenced(mtrack.chords, mtrack.tuplets),
|
|
"MidiVoice::separateVoices",
|
|
"Not all tuplets are referenced in chords or notes "
|
|
"after voice sort");
|
|
Q_ASSERT_X(areVoicesSame(mtrack.chords),
|
|
"MidiVoice::separateVoices", "Different voices of chord and tuplet "
|
|
"after voice sort");
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
} // namespace MidiVoice
|
|
} // namespace Ms
|