311 lines
12 KiB
C++
311 lines
12 KiB
C++
#include "importmidi_chord.h"
|
|
#include "importmidi_inner.h"
|
|
#include "importmidi_chord.h"
|
|
#include "importmidi_clef.h"
|
|
#include "libmscore/mscore.h"
|
|
|
|
#include <set>
|
|
|
|
|
|
namespace Ms {
|
|
namespace MChord {
|
|
|
|
|
|
ReducedFraction minAllowedDuration()
|
|
{
|
|
const static auto minDuration = ReducedFraction::fromTicks(MScore::division) / 32;
|
|
return minDuration;
|
|
}
|
|
|
|
ReducedFraction maxNoteLen(const QList<MidiNote> ¬es)
|
|
{
|
|
ReducedFraction maxLen(0, 1);
|
|
for (const auto ¬e: notes) {
|
|
if (note.len > maxLen)
|
|
maxLen = note.len;
|
|
}
|
|
return maxLen;
|
|
}
|
|
|
|
int findAveragePitch(const QList<MidiNote> ¬es)
|
|
{
|
|
int avgPitch = 0;
|
|
for (const auto ¬e: notes)
|
|
avgPitch += note.pitch;
|
|
return notes.size() ? avgPitch / notes.size() : 0;
|
|
}
|
|
|
|
int findAveragePitch(const std::map<ReducedFraction, MidiChord>::const_iterator &startChordIt,
|
|
const std::map<ReducedFraction, MidiChord>::const_iterator &endChordIt)
|
|
{
|
|
int avgPitch = 0;
|
|
int counter = 0;
|
|
for (auto it = startChordIt; it != endChordIt; ++it) {
|
|
avgPitch += findAveragePitch(it->second.notes);
|
|
++counter;
|
|
}
|
|
if (counter)
|
|
avgPitch /= counter;
|
|
if (avgPitch == 0)
|
|
avgPitch = MidiClef::midPitch();
|
|
return avgPitch;
|
|
}
|
|
|
|
// remove overlapping notes with the same pitch
|
|
|
|
void removeOverlappingNotes(std::multimap<int, MTrack> &tracks)
|
|
{
|
|
for (auto &track: tracks) {
|
|
auto &chords = track.second.chords;
|
|
for (auto it = chords.begin(); it != chords.end(); ++it) {
|
|
auto &firstChord = it->second;
|
|
const auto &firstOnTime = it->first;
|
|
for (auto ¬e1: firstChord.notes) {
|
|
auto ii = std::next(it);
|
|
for (; ii != chords.end(); ++ii) {
|
|
auto &secondChord = ii->second;
|
|
if (firstChord.voice != secondChord.voice)
|
|
continue;
|
|
const auto &secondOnTime = ii->first;
|
|
for (auto ¬e2: secondChord.notes) {
|
|
if (note2.pitch != note1.pitch)
|
|
continue;
|
|
if (secondOnTime >= (firstOnTime + note1.len))
|
|
continue;
|
|
qDebug("Midi import: overlapping events: %d+%d %d+%d",
|
|
firstOnTime.ticks(), note1.len.ticks(),
|
|
secondOnTime.ticks(), note2.len.ticks());
|
|
note1.len = secondOnTime - firstOnTime;
|
|
ii = std::prev(chords.end());
|
|
break;
|
|
}
|
|
}
|
|
if (note1.len <= ReducedFraction(0, 1)) {
|
|
qDebug("Midi import: duration <= 0: drop note at %d",
|
|
firstOnTime.ticks());
|
|
continue;
|
|
}
|
|
}
|
|
} // for note1
|
|
}
|
|
}
|
|
|
|
|
|
#ifdef QT_DEBUG
|
|
|
|
bool areOnTimeValuesDifferent(const std::multimap<ReducedFraction, MidiChord> &chords)
|
|
{
|
|
std::set<ReducedFraction> onTimes;
|
|
for (const auto &chordEvent: chords) {
|
|
if (onTimes.find(chordEvent.first) == onTimes.end())
|
|
onTimes.insert(chordEvent.first);
|
|
else
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool areSingleNoteChords(const std::multimap<ReducedFraction, MidiChord> &chords)
|
|
{
|
|
for (const auto &chordEvent: chords) {
|
|
if (chordEvent.second.notes.size() > 1)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
// based on quickthresh algorithm
|
|
//
|
|
// http://www.cycling74.com/docs/max5/refpages/max-ref/quickthresh.html
|
|
// (link date 9 July 2013)
|
|
//
|
|
// here are default values for audio, in milliseconds
|
|
// for midi there will be another values, in ticks
|
|
|
|
// all notes received in the left inlet within this time period are collected into a chord
|
|
// threshTime = 40 ms
|
|
|
|
// if there are any incoming values within this amount of time
|
|
// at the end of the base thresh time,
|
|
// the threshold is extended to allow more notes to be added to the chord
|
|
// fudgeTime = 10 ms
|
|
|
|
// this is an extension value of the base thresh time, which is used if notes arrive
|
|
// in the object's inlet in the "fudge" time zone
|
|
// threshExtTime = 20 ms
|
|
|
|
void collectChords(std::multimap<int, MTrack> &tracks)
|
|
{
|
|
for (auto &track: tracks) {
|
|
auto &chords = track.second.chords;
|
|
if (chords.empty())
|
|
continue;
|
|
|
|
const ReducedFraction threshTime = minAllowedDuration() / 2;
|
|
const ReducedFraction fudgeTime = threshTime / 4;
|
|
const ReducedFraction threshExtTime = threshTime / 2;
|
|
|
|
ReducedFraction currentChordStart(-1, 1); // invalid
|
|
ReducedFraction curThreshTime(-1, 1);
|
|
// if note onTime goes after max chord offTime
|
|
// then this is not a chord but arpeggio
|
|
ReducedFraction maxOffTime(-1, 1);
|
|
|
|
// chords here should consist of a single note
|
|
// because notes are not united into chords yet
|
|
Q_ASSERT_X(areSingleNoteChords(chords),
|
|
"MChord: collectChords", "Some chords have more than one note");
|
|
|
|
for (auto it = chords.begin(); it != chords.end(); ) {
|
|
const auto ¬e = it->second.notes[0];
|
|
|
|
// short events with len < minAllowedDuration must be cleaned up
|
|
Q_ASSERT_X(note.len >= minAllowedDuration(),
|
|
"MChord: collectChords", "Note length is less than min allowed duration");
|
|
|
|
if (it->first <= currentChordStart + curThreshTime) {
|
|
|
|
// this branch should not be executed when it == chords.begin()
|
|
Q_ASSERT_X(it != chords.begin(),
|
|
"MChord: collectChords", "it == chords.begin()");
|
|
|
|
if (it->first < maxOffTime) {
|
|
// add current note to the previous chord
|
|
auto prev = std::prev(it);
|
|
prev->second.notes.push_back(note);
|
|
if (it->first >= currentChordStart + curThreshTime - fudgeTime
|
|
&& curThreshTime == threshTime) {
|
|
curThreshTime += threshExtTime;
|
|
}
|
|
if (it->first + note.len > maxOffTime)
|
|
maxOffTime = it->first + note.len;
|
|
it = chords.erase(it);
|
|
continue;
|
|
}
|
|
}
|
|
currentChordStart = it->first;
|
|
maxOffTime = currentChordStart + note.len;
|
|
curThreshTime = threshTime;
|
|
++it;
|
|
}
|
|
|
|
Q_ASSERT_X(areOnTimeValuesDifferent(chords),
|
|
"MChord: collectChords", "onTime values of chords are equal "
|
|
"but should be different");
|
|
}
|
|
}
|
|
|
|
void sortNotesByPitch(std::multimap<ReducedFraction, MidiChord> &chords)
|
|
{
|
|
struct {
|
|
bool operator()(const MidiNote ¬e1, const MidiNote ¬e2)
|
|
{
|
|
return note1.pitch < note2.pitch;
|
|
}
|
|
} pitchSort;
|
|
|
|
for (auto &chordEvent: chords) {
|
|
// in each chord sort notes by pitches
|
|
auto ¬es = chordEvent.second.notes;
|
|
qSort(notes.begin(), notes.end(), pitchSort);
|
|
}
|
|
}
|
|
|
|
void sortNotesByLength(std::multimap<ReducedFraction, MidiChord> &chords)
|
|
{
|
|
struct {
|
|
bool operator()(const MidiNote ¬e1, const MidiNote ¬e2)
|
|
{
|
|
return note1.len < note2.len;
|
|
}
|
|
} lenSort;
|
|
|
|
for (auto &chordEvent: chords) {
|
|
// in each chord sort notes by pitches
|
|
auto ¬es = chordEvent.second.notes;
|
|
qSort(notes.begin(), notes.end(), lenSort);
|
|
}
|
|
}
|
|
|
|
// find notes of each chord that have different durations
|
|
// and separate them into different chords
|
|
// so all notes inside every chord will have equal lengths
|
|
|
|
void splitUnequalChords(std::multimap<int, MTrack> &tracks)
|
|
{
|
|
for (auto &track: tracks) {
|
|
std::vector<std::pair<ReducedFraction, MidiChord>> newChordEvents;
|
|
auto &chords = track.second.chords;
|
|
sortNotesByLength(chords);
|
|
for (auto &chordEvent: chords) {
|
|
auto &chord = chordEvent.second;
|
|
auto ¬es = chord.notes;
|
|
ReducedFraction len;
|
|
for (auto it = notes.begin(); it != notes.end(); ) {
|
|
if (it == notes.begin())
|
|
len = it->len;
|
|
else {
|
|
ReducedFraction newLen = it->len;
|
|
if (newLen != len) {
|
|
MidiChord newChord(chord);
|
|
newChord.notes.clear();
|
|
for (int j = it - notes.begin(); j > 0; --j)
|
|
newChord.notes.push_back(notes[j - 1]);
|
|
newChordEvents.push_back({chordEvent.first, newChord});
|
|
it = notes.erase(notes.begin(), it);
|
|
continue;
|
|
}
|
|
}
|
|
++it;
|
|
}
|
|
}
|
|
for (const auto &event: newChordEvents)
|
|
chords.insert(event);
|
|
}
|
|
}
|
|
|
|
ReducedFraction findMinDuration(const QList<MidiChord> &midiChords,
|
|
const ReducedFraction &length)
|
|
{
|
|
ReducedFraction len = length;
|
|
for (const auto &chord: midiChords) {
|
|
for (const auto ¬e: chord.notes) {
|
|
if ((note.len < len) && (note.len != ReducedFraction(0, 1)))
|
|
len = note.len;
|
|
}
|
|
}
|
|
return len;
|
|
}
|
|
|
|
void mergeChordsWithEqualOnTimeAndVoice(std::multimap<int, MTrack> &tracks)
|
|
{
|
|
for (auto &track: tracks) {
|
|
auto &chords = track.second.chords;
|
|
// the key is pair<onTime, voice>
|
|
std::map<std::pair<ReducedFraction, int>,
|
|
std::multimap<ReducedFraction, MidiChord>::iterator> onTimes;
|
|
|
|
for (auto it = chords.begin(); it != chords.end(); ) {
|
|
const auto &onTime = it->first;
|
|
const int voice = it->second.voice;
|
|
auto fit = onTimes.find({onTime, voice});
|
|
if (fit == onTimes.end()) {
|
|
onTimes.insert({{onTime, voice}, it});
|
|
}
|
|
else {
|
|
auto &oldNotes = fit->second->second.notes;
|
|
auto &newNotes = it->second.notes;
|
|
oldNotes.append(newNotes);
|
|
it = chords.erase(it);
|
|
continue;
|
|
}
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace MChord
|
|
} // namespace Ms
|