2024-07-15 11:46:59 +08:00

432 lines
12 KiB
C++

//
// Programmer: Craig Stuart Sapp <craig@ccrma.stanford.edu>
// Creation Date: Sun Feb 7 11:37:52 PST 2021
// Last Modified: Sun Feb 7 14:12:31 PST 2021
// Filename: tools/deltatimes.cpp
// URL: https://github.com/craigsapp/midifile/blob/master/tools/deltatimes.cpp
// Syntax: C++11
// vim: ts=3
//
// Description: Generate list of delta times in a MIDI file, either as MIDI ticks,
// quarter notes, or at real-time values in seconds or milliseconds
// (why applying tempo meta messages to the tick values). Useful
// for determining if the MIDI file contains a score or a performance.
//
// Options:
// -q Output delta times as quarter note units instead of MIDI ticks.
// -s Output real times in seconds.
// -m Output real times in milliseconds.
// -a Output MIDI tick, quarter note, second and millisecond timings.
// -c Count timings by value (output a delta-time histogram).
// -r Reverse: place the count after the timing values in the histogram.
// -n # Normalize by the maximum count in the histogram scaled to number #.
// -N # Normalize by the sum of the histogram counts scaled to number #.
// -C Sort histogram by counts (largest to smallest).
// --tpq Print ticks-per-quarter note from MIDI header(s) only.
// --notes Calculate for MIDI notes only.
// --on Calculate for MIDI notes-ons only.
//
#include "MidiFile.h"
#include "Options.h"
#include <algorithm>
#include <functional>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
using namespace std;
using namespace smf;
#define TIME_TICKS 0
#define TIME_SECONDS 1
#define TIME_MILLISECONDS 2
#define TIME_QUARTERS 3
void processFile(MidiFile& midifile, map<string, int>& histogram, Options& options);
void printHistogram(map<string, int>& histogram, Options& options);
void extractDeltaTimesForAllEvents(MidiFile& midifile, map<string, int>& histogram, Options& options);
void extractDeltaTimesForAllNotes(MidiFile& midifile, map<string, int>& histogram, Options& options);
void extractDeltaTimesForAllNotesOns(MidiFile& midifile, map<string, int>& histogram, Options& options);
///////////////////////////////////////////////////////////////////////////
int main(int argc, char** argv) {
Options options;
options.define("a|all=b", "output all types of time values for delta times");
options.define("s|sec|second|seconds=b", "display times in seconds");
options.define("m|ms|millisecond|milliseconds=b", "display times in milliseconds");
options.define("q|quarter|quarters|quarter-notes|quarter-note=b", "display total time in quarter notes");
options.define("c|count|counts=b", "display times in a histogram");
options.define("n|normalize-by-max=d:100.0", "Normalize to max value in histogram");
options.define("N|normalize-by-total=d:100.0", "Normalize to total count in histogram");
options.define("r|reverse=b", "reverse histogram label/count order");
options.define("C|sort-by-count=b", "sort histogram by counts rather than times");
options.define("tpq=b", "print only the ticks-per-quarter note value from the MIDI header");
options.define("notes|note=b", "delta times only between note on/offs");
options.define("on|note-on|note-ons=b", "delta times only between note ons");
options.process(argc, argv);
MidiFile midifile;
map<string, int> histogram;
if (options.getArgCount() == 0) {
midifile.read(cin);
processFile(midifile, histogram, options);
} else {
int count = options.getArgCount();
for (int i=0; i<count; i++) {
string filename = options.getArg(i+1);
midifile.read(filename);
processFile(midifile, histogram, options);
}
}
if (options.getBoolean("count") && !options.getBoolean("tpq")) {
printHistogram(histogram, options);
}
return 0;
}
///////////////////////////////////////////////////////////////////////////
//////////////////////////////
//
// processFile -- Extract delta timings from the MIDI file, either
// as raw ticks or as real times interpreted from tempo meta messages.
//
void processFile(MidiFile& midifile, map<string, int>& histogram, Options& options) {
if (options.getBoolean("tpq")) {
int tpq = midifile.getTicksPerQuarterNote();
cerr << tpq << endl;
return;
}
if (options.getBoolean("notes")) {
extractDeltaTimesForAllNotes(midifile, histogram, options);
} else if (options.getBoolean("on")) {
extractDeltaTimesForAllNotesOns(midifile, histogram, options);
} else {
extractDeltaTimesForAllEvents(midifile, histogram, options);
}
}
//////////////////////////////
//
// extractDeltaTimesForAllEvents -- calculate delta timings between all events.
//
void extractDeltaTimesForAllEvents(MidiFile& midifile, map<string, int>& histogram,
Options& options) {
int tpq = midifile.getTicksPerQuarterNote();
int timeFormat = TIME_TICKS;
bool realtimeQ = false;
bool allQ = options.getBoolean("all");
if (allQ) {
realtimeQ = true;
}
if (options.getBoolean("seconds")) {
timeFormat = TIME_SECONDS;
realtimeQ = true;
} else if (options.getBoolean("milliseconds")) {
timeFormat = TIME_MILLISECONDS;
realtimeQ = true;
} else if (options.getBoolean("quarters")) {
timeFormat = TIME_QUARTERS;
}
bool histogramQ = options.getBoolean("count");
if (realtimeQ) {
midifile.doTimeAnalysis();
}
midifile.makeDeltaTicks();
for (int i=0; i<midifile.getTrackCount(); i++) {
for (int j=0; j<midifile[i].getEventCount(); j++) {
double value;
double newtime;
double oldtime;
stringstream key;
if (allQ) {
key.str().clear();
value = midifile[i][j].tick;
key << value << "\t"; // MIDI ticks
key << value/tpq << "\t"; // quarter notes
newtime = midifile[i][j].seconds;
if (j > 0) {
oldtime = midifile[i][j-1].seconds;
} else {
oldtime = 0.0;
}
value = newtime - oldtime;
key << value << "\t"; // seconds
key << value * 1000.0; // milliseconds
} else if (realtimeQ) {
newtime = midifile[i][j].seconds;
if (j == 0) {
oldtime = 0;
} else {
oldtime = midifile[i][j-1].seconds;
}
value = newtime - oldtime;
if (timeFormat == TIME_MILLISECONDS) {
value *= 1000.0;
}
key.str().clear();
key << value;
} else {
value = midifile[i][j].tick;
if (timeFormat == TIME_QUARTERS) {
value /= tpq;
}
key.str().clear();
key << value;
}
if (histogramQ) {
histogram[key.str()]++;
} else {
cout << key.str() << endl;
}
}
}
}
//////////////////////////////
//
// extractDeltaTimesForAllNotes -- calculate delta timings between all note MIDI
// messages (ignoring other MIDI messages).
//
void extractDeltaTimesForAllNotes(MidiFile& midifile, map<string, int>& histogram,
Options& options) {
int tpq = midifile.getTicksPerQuarterNote();
int timeFormat = TIME_TICKS;
bool realtimeQ = false;
bool allQ = options.getBoolean("all");
int lastindex = -1;
if (allQ) {
realtimeQ = true;
}
if (options.getBoolean("seconds")) {
timeFormat = TIME_SECONDS;
realtimeQ = true;
} else if (options.getBoolean("milliseconds")) {
timeFormat = TIME_MILLISECONDS;
realtimeQ = true;
} else if (options.getBoolean("quarters")) {
timeFormat = TIME_QUARTERS;
}
bool histogramQ = options.getBoolean("count");
if (realtimeQ) {
midifile.doTimeAnalysis();
}
for (int i=0; i<midifile.getTrackCount(); i++) {
lastindex = -1;
for (int j=0; j<midifile[i].getEventCount(); j++) {
if (!midifile[i][j].isNote()) {
continue;
}
if (lastindex < 0) {
lastindex = j;
continue;
}
double value;
stringstream key;
if (allQ) {
key.str().clear();
value = midifile[i][j].tick - midifile[i][lastindex].tick;
key << value << "\t"; // MIDI ticks
key << value/tpq << "\t"; // quarter notes
value = midifile[i][j].seconds - midifile[i][lastindex].seconds;
key << value << "\t"; // seconds
key << value * 1000.0; // milliseconds
} else if (realtimeQ) {
value = midifile[i][j].seconds - midifile[i][lastindex].seconds;
if (timeFormat == TIME_MILLISECONDS) {
value *= 1000.0;
}
key.str().clear();
key << value;
} else {
value = midifile[i][j].tick - midifile[i][lastindex].tick;
if (timeFormat == TIME_QUARTERS) {
value /= tpq;
}
key.str().clear();
key << value;
}
if (histogramQ) {
histogram[key.str()]++;
} else {
cout << key.str() << endl;
}
lastindex = j;
}
}
}
//////////////////////////////
//
// extractDeltaTimesForAllNoteOns -- calculate delta timings between all note-on MIDI
// messages (ignoring other MIDI messages).
//
void extractDeltaTimesForAllNotesOns(MidiFile& midifile, map<string, int>& histogram,
Options& options) {
int tpq = midifile.getTicksPerQuarterNote();
int timeFormat = TIME_TICKS;
bool realtimeQ = false;
bool allQ = options.getBoolean("all");
int lastindex = -1;
if (allQ) {
realtimeQ = true;
}
if (options.getBoolean("seconds")) {
timeFormat = TIME_SECONDS;
realtimeQ = true;
} else if (options.getBoolean("milliseconds")) {
timeFormat = TIME_MILLISECONDS;
realtimeQ = true;
} else if (options.getBoolean("quarters")) {
timeFormat = TIME_QUARTERS;
}
bool histogramQ = options.getBoolean("count");
if (realtimeQ) {
midifile.doTimeAnalysis();
}
for (int i=0; i<midifile.getTrackCount(); i++) {
lastindex = -1;
for (int j=0; j<midifile[i].getEventCount(); j++) {
if (!midifile[i][j].isNoteOn()) {
continue;
}
if (lastindex < 0) {
lastindex = j;
continue;
}
double value;
stringstream key;
if (allQ) {
key.str().clear();
value = midifile[i][j].tick - midifile[i][lastindex].tick;
key << value << "\t"; // MIDI ticks
key << value/tpq << "\t"; // quarter notes
value = midifile[i][j].seconds - midifile[i][lastindex].seconds;
key << value << "\t"; // seconds
key << value * 1000.0; // milliseconds
} else if (realtimeQ) {
value = midifile[i][j].seconds - midifile[i][lastindex].seconds;
if (timeFormat == TIME_MILLISECONDS) {
value *= 1000.0;
}
key.str().clear();
key << value;
} else {
value = midifile[i][j].tick - midifile[i][lastindex].tick;
if (timeFormat == TIME_QUARTERS) {
value /= tpq;
}
key.str().clear();
key << value;
}
if (histogramQ) {
histogram[key.str()]++;
} else {
cout << key.str() << endl;
}
lastindex = j;
}
}
}
//////////////////////////////
//
// printHistogram --
//
void printHistogram(map<string, int>& histogram, Options& options) {
bool reverseQ = options.getBoolean("reverse");
double scale = 1.0;
if (options.getBoolean("normalize-by-max")) {
int maximum = 0;
for (auto it : histogram) {
if (it.second > maximum) {
maximum = it.second;
}
}
if (maximum > 0) {
scale = options.getDouble("normalize-by-max") / maximum;
}
} else if (options.getBoolean("normalize-by-total")) {
int sum = 0;
for (auto it : histogram) {
sum += it.second;
}
if (sum > 0) {
scale = options.getDouble("normalize-by-total") / sum;
}
}
if (options.getBoolean("sort-by-count")) {
// Sort by count, from lowest to highest:
typedef function<bool(pair<int, string>, pair<int, string>)> Comparator;
Comparator sortFunction = [](pair<int, string> a, pair<int, string> b) {
if (a.first == b.first) {
return stod(a.second) < stod(b.second);
} else {
return a.first > b.first;
}
};
map<pair<int, string>, int, Comparator> sortedkeys(sortFunction);;
for (auto it : histogram) {
sortedkeys[make_pair(it.second, it.first)] = it.second;
}
for (auto it : sortedkeys) {
string label = it.first.second;
double count = it.second * scale;
if (reverseQ) {
cout << label << '\t' << count << endl;
} else {
cout << count << '\t' << label << endl;
}
}
} else {
// Sort by delta time, from lowest to highest:
map<double, string, less<double>> sortedkeys;
for (auto it : histogram) {
sortedkeys[stod(it.first)] = it.first;
}
for (auto it : sortedkeys) {
string label = it.second;
double count = histogram[it.second] * scale;
if (reverseQ) {
cout << label << '\t' << count << endl;
} else {
cout << count << '\t' << label << endl;
}
}
}
}