Files
AronaCore/third_party/midifile/tools/midi2chords.cpp
2024-07-15 11:46:59 +08:00

548 lines
13 KiB
C++

//
// Programmer: Craig Stuart Sapp <craig@ccrma.stanford.edu>
// Creation Date: Mon Oct 24 18:54:19 PDT 2022
// Last Modified: Tue Oct 25 08:52:05 PDT 2022
// Filename: tools/midi2chords.cpp
// URL: https://github.com/craigsapp/midifile/blob/master/tools/midi2chords.cpp
// Syntax: C++11
// vim: ts=3
//
// Description: Identify chord sequences in MIDI files. Whenever
// there are three or more note attacks at a given
// time, a chord will be identified (currently notes must
// all attack all at the same tick time). Additional chord
// identifications can be added to the
// Sonority::identifySonority() function.
//
#include "Options.h"
#include "MidiFile.h"
#include <iostream>
using namespace std;
using namespace smf;
///////////////////////////////////////////////////////////////////////////
class Sonority {
public:
double qstamp = 0.0; // quarter-note timestamp
double qdur = 0.0; // duration to next chord
vector<int> attacks; // MIDI notes in sonority (attacking)
vector<int> sustains; // Sustaining notes (128 states).
// attackPcs: number of notes at each pitch class that are
// attacking (starting).
vector<int> attackPcs = {0,0,0,0,0,0,0,0,0,0,0,0};
// sustainPcs: number of notes that are carrying over from
// a previous sonority.
vector<int> sustainPcs = {0,0,0,0,0,0,0,0,0,0,0,0};
string root; // root pitch of sonority
string quality; // type of chord
static bool sustainQ; // process sustained notes as well
Sonority(void);
void addNoteAttack(int pitch);
int countPcs(void);
void clear(void);
void identifySonority(void);
void setChordLabel(int pc, const string& qual);
static int checkForMatch(vector<int>& data, vector<int>& prototype);
};
bool Sonority::sustainQ = true; // Consider sustains by default
Sonority::Sonority(void) {
sustains.resize(128);
fill(sustains.begin(), sustains.end(), 0);
}
void Sonority::addNoteAttack(int pitch) {
attacks.push_back(pitch);
int pc = pitch % 12;
attackPcs.at(pc)++;
}
int Sonority::countPcs(void) {
int output = 0;
for (int i=0; i<(int)attackPcs.size(); i++) {
if (attackPcs.at(i)) {
output++;
} else if (sustainQ && sustainPcs.at(i)) {
output++;
}
}
return output;
}
void Sonority::clear(void) {
qstamp = 0.0;
qdur = 0.0;
attacks.clear();
fill(sustains.begin(), sustains.end(), 0);
fill(attackPcs.begin(), attackPcs.end(), 0);
fill(sustainPcs.begin(), sustainPcs.end(), 0);
}
void Sonority::identifySonority(void) {
int match;
vector<int> pcs(12, 0);
for (int i=0; i<(int)attackPcs.size(); i++) {
pcs.at(i) += attackPcs.at(i);
}
if (sustainQ) {
for (int i=0; i<(int)sustainPcs.size(); i++) {
pcs.at(i) += sustainPcs.at(i);
}
}
// convert to booleans:
for (int i=0; i<(int)pcs.size(); i++) {
pcs.at(i) = !!pcs.at(i);
}
// Triadic chord:
vector<int> major = {1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0};
match = Sonority::checkForMatch(pcs, major);
if (match >= 0) {
setChordLabel(match, "major");
return;
}
vector<int> minor = {1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0};
match = Sonority::checkForMatch(pcs, minor);
if (match >= 0) {
setChordLabel(match, "minor");
return;
}
vector<int> diminished = {1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0};
match = Sonority::checkForMatch(pcs, diminished);
if (match >= 0) {
setChordLabel(match, "diminished");
return;
}
vector<int> augmented = {1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0};
match = Sonority::checkForMatch(pcs, augmented);
if (match >= 0) {
// Note that the augmented root is ambiguious for MIDI:
// the chord is symmetric, and any not could be the root.
setChordLabel(match, "augmented");
return;
}
// Seventh chords:
vector<int> majmin = {1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0};
match = Sonority::checkForMatch(pcs, majmin);
if (match >= 0) {
setChordLabel(match, "dominant seventh");
return;
}
vector<int> minmin = {1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0};
match = Sonority::checkForMatch(pcs, minmin);
if (match >= 0) {
setChordLabel(match, "minor seventh");
return;
}
vector<int> majmaj = {1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1};
match = Sonority::checkForMatch(pcs, majmaj);
if (match >= 0) {
setChordLabel(match, "major seventh");
return;
}
vector<int> minmaj = {1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1};
match = Sonority::checkForMatch(pcs, minmaj);
if (match >= 0) {
setChordLabel(match, "minor-major seventh");
return;
}
vector<int> halfdim = {1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0};
match = Sonority::checkForMatch(pcs, halfdim);
if (match >= 0) {
setChordLabel(match, "half diminished seventh");
return;
}
vector<int> fulldim = {1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0};
match = Sonority::checkForMatch(pcs, fulldim);
if (match >= 0) {
setChordLabel(match, "fully diminished seventh");
return;
}
// Other chord definitions can be added here.
}
void Sonority::setChordLabel(int pc, const string& qual) {
quality = qual;
switch (pc) {
case 0: root = "C"; break;
case 1: root = "C#"; break;
case 2: root = "D"; break;
case 3: root = "E-"; break;
case 4: root = "E"; break;
case 5: root = "F"; break;
case 6: root = "F#"; break;
case 7: root = "G"; break;
case 8: root = "A-"; break;
case 9: root = "A"; break;
case 10: root = "B-"; break;
case 11: root = "B"; break;
}
}
int Sonority::checkForMatch(vector<int>& data, vector<int>& prototype) {
// returns -1 if no match found; otherwise, returns pitch class
// of match (0 = C, 1 = C#, ..., 11 = B).
int output = -1;
bool match = false;
if (data.size() != 12) {
return -1;
}
if (prototype.size() != 12) {
return -1;
}
for (int i=0; i<12; i++) {
match = true;
output = i;
for (int j=0; j<12; j++) {
if ((!!prototype.at(j)) != data.at((i+j) % 12)) {
match = false;
break;
}
}
if (match) {
break;
}
}
return match ? output : -1;
}
///////////////////////////////////////////////////////////////////////////
void processMidiFile (Options& options, MidiFile& midifile);
void calculateDurations (vector<Sonority>& chordSequence, double maxQstamp);
void printChordSequence (vector<Sonority>& chordSequence);
int getActiveNoteCount (vector<int>& states);
void printPcInfo (Sonority& sonority);
///////////////////////////////////////////////////////////////////////////
int main(int argc, char** argv) {
int status;
MidiFile inputfile;
Options options(argc, argv);
options.define("S|no-sustains=b", "don't consider sustained notes");
options.process(argc, argv);
int argcount = options.getArgCount();
if (argcount != 1) {
cerr << "Usage: " << options.getCommand() << " midifile" << endl;
exit(1);
}
status = inputfile.read(options.getArg(1));
if (status == 0) {
cout << "Syntax error in file: " << options.getArg(1) << "\n";
}
processMidiFile(options, inputfile);
return 0;
}
///////////////////////////////////////////////////////////////////////////
//////////////////////////////
//
// processMidiFile --
//
void processMidiFile(Options& options, MidiFile& midifile) {
Sonority::sustainQ = !options.getBoolean("no-sustains");
midifile.absoluteTicks(); // convert from delta time to absolute time
midifile.joinTracks(); // merge all notes to single timeline.
vector<Sonority> chordSequence;
chordSequence.reserve(10000);
vector<int> attackPcs(12, 0);
vector<int> sustainPcs(12, 0);
vector<int> noteStates(128, 0);
Sonority currentChord;
double tpq = midifile.getTicksPerQuarterNote();
int eventcount = midifile.getEventCount(0);
MidiEvent *event;
for (int i=0; i<eventcount; i++) {
event = &(midifile[0][i]);
if (event->getChannel() == 0x09) {
// ignore General MIDI drum tracks
continue;
}
if (!event->isNote()) {
// ignore events that are not note-ons or note-offs
continue;
}
int pitch = event->getKeyNumber();
int pc = pitch % 12;
if (event->isNoteOff()) {
// remove note from sustain buffer
sustainPcs.at(pc)--;
noteStates.at(pitch)--;
cerr << "removing note from sustains" << pitch << endl;
// negative values in sustainPcs are strange, so print
// waring and clip to zero:
if (sustainPcs.at(pc) < 0) {
sustainPcs.at(pc) = 0;
cerr << "Strange case of excess note-offs" << endl;
}
if (noteStates.at(pitch) < 0) {
noteStates.at(pitch) = 0;
cerr << "Strange case of excess note-offs, case 2" << endl;
}
continue;
}
sustainPcs.at(pc)++;
noteStates.at(pitch)++;
cerr << "adding note to sustains" << pitch << endl;
double qstamp = event->tick / tpq;
if (qstamp == currentChord.qstamp) {
// Add a note to the current chord:
currentChord.addNoteAttack(pitch);
cerr << "ADDING NOTE ATTACK TO CURRENT CHORD: " << pitch << endl;
} else if (qstamp > currentChord.qstamp) {
// Store the current chord in the array
// if there are at least 3 pitch classes:
int activeNoteCount = getActiveNoteCount(sustainPcs);
if (activeNoteCount >= 3) {
cerr << "STORING CHORD IN LIST" << endl;
chordSequence.push_back(currentChord);
chordSequence.back().sustains = sustainPcs;
chordSequence.back().sustainPcs = sustainPcs;
}
cerr << "===========================" << endl;
currentChord.clear();
currentChord.qstamp = qstamp;
currentChord.addNoteAttack(pitch);
cerr << "ADDING NOTE ATTACK TO CURRENT CHORD: " << pitch << endl;
} else {
cerr << "Causality violation at qstamp " << qstamp << endl;
}
}
// Add last chord in file:
int activeNoteCount = getActiveNoteCount(sustainPcs);
if (activeNoteCount >= 3) {
cerr << "STORING CHORD IN LIST END" << endl;
chordSequence.push_back(currentChord);
chordSequence.back().sustains = noteStates;
chordSequence.back().sustainPcs = sustainPcs;
}
double maxQstamp = midifile[0].back().tick / tpq;
calculateDurations(chordSequence, maxQstamp);
for (int i=0; i<(int)chordSequence.size(); i++) {
chordSequence[i].identifySonority();
}
printChordSequence(chordSequence);
}
//////////////////////////////
//
// getActiveNoteCount --
//
int getActiveNoteCount(vector<int>& states) {
int output = 0;
for (int i=0; i<(int)states.size(); i++) {
output++;
}
return output;
}
//////////////////////////////
//
// calculateDurations --
//
void calculateDurations(vector<Sonority>& chordSequence, double maxQstamp) {
vector<Sonority>& cs = chordSequence;
for (int i=0; i<(int)cs.size() - 1; i++) {
cs[i].qdur = cs[i+1].qstamp - cs[i].qstamp;
}
// calculate last chord duration:
cs.back().qdur = maxQstamp - cs.back().qstamp;
}
/////////////////////////////
//
// printChordSequence --
//
void printChordSequence(vector<Sonority>& chordSequence) {
vector<Sonority>& cs = chordSequence;
for (int i=0; i<(int)cs.size(); i++) {
cout << cs[i].qstamp << "\t" << cs[i].qdur << ":\t";
printPcInfo(cs[i]);
cout << ":\t" << cs[i].root;
cout << "\t" << cs[i].quality;
cout << endl;
}
}
//////////////////////////////
//
// printPcInfo --
//
void printPcInfo(Sonority& sonority) {
for (int i=0; i<(int)sonority.attackPcs.size(); i++) {
bool attack = (bool)sonority.attackPcs.at(i);
bool sustain = (bool)sonority.sustainPcs.at(i);
sustain ^= attack;
bool off = !(sustain || attack);
char letter;
switch (i) {
case 0: letter = 'C'; break;
case 2: letter = 'D'; break;
case 4: letter = 'E'; break;
case 5: letter = 'F'; break;
case 7: letter = 'G'; break;
case 9: letter = 'A'; break;
case 11: letter = 'B'; break;
default: letter = '1';
}
if (off) {
letter = '0';
}
if (sustain) {
letter = tolower(letter);
if (letter == '1') {
letter = 'i';
}
}
cout << letter;
}
}
/* test file: Save content below to chordtest.txt and then run the command:
* bin/binasc chordtest -c chordtest.mid
* then test with the command:
* bin/midi2chords chordtest.mid
* results:
* 0 1: 100010010000: C major
* 1 1: 100001000100: F major
* 2 1: 100010010000: C major
* 3 1: 001001010001: G dominant seventh
* 4 1: 100010010000: C major
*
* First number is the time in quarter notes since the
* start of the MIDI file. Second number is the duration
* to the next chord (in quarter notes), then a 12-digit
* list of pitch classes (first is C, then C#, etc. to B).
* Lastly is the identified chord (if found in list of defined
* chords).
"MThd" ; MIDI header chunk marker
4'6 ; bytes to follow in header chunk
2'0 ; file format: Type-0 (single track)
2'1 ; number of tracks
2'100 ; ticks per quarter note
;;; TRACK 0 ----------------------------------
"MTrk" ; MIDI track chunk marker
4'132 ; bytes to follow in track chunk
v0 90 '60 '60 ; note-on C4
v0 90 '64 '60 ; note-on E4
v0 90 '67 '60 ; note-on G4
v100 90 '60 '0 ; note-off C4
v0 90 '64 '0 ; note-off E4
v0 90 '67 '0 ; note-off G4
v0 90 '60 '60 ; note-on C4
v0 90 '65 '60 ; note-on F4
v0 90 '69 '60 ; note-on A4
v100 90 '60 '0 ; note-off C4
v0 90 '65 '0 ; note-off F4
v0 90 '69 '0 ; note-off A4
v0 90 '60 '60 ; note-on C4
v0 90 '64 '60 ; note-on E4
v0 90 '67 '60 ; note-on G4
v100 90 '60 '0 ; note-off C4
v0 90 '64 '0 ; note-off E4
v0 90 '67 '0 ; note-off G4
v0 90 '59 '60 ; note-on B3
v0 90 '62 '60 ; note-on D4
v0 90 '65 '60 ; note-on F4
v0 90 '67 '60 ; note-on G4
v100 90 '59 '0 ; note-off B3
v0 90 '62 '0 ; note-off D4
v0 90 '65 '0 ; note-off F4
v0 90 '67 '0 ; note-off G4
v0 90 '60 '60 ; note-on C4
v0 90 '64 '60 ; note-on E4
v0 90 '67 '60 ; note-on G4
v100 90 '60 '0 ; note-off C4
v0 90 '64 '0 ; note-off E4
v0 90 '67 '0 ; note-off G4
v0 ff 2f v0 ; end-of-track
*/