maia-markov
Version:
Markov analysis and generation functions supporting various applications by Music Artificial Intelligence Algorithms, Inc.
507 lines (455 loc) • 18.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _Analyzer = require('./Analyzer');
var _Analyzer2 = _interopRequireDefault(_Analyzer);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
// Imports
// import fs
var fs = require('fs');
var pa = require('path');
var _require = require('@tonejs/midi'),
Midi = _require.Midi;
var mu = require('maia-util');
/**
* Class for importing MIDI files and extracting information from them.
*/
var MidiImport = function () {
/**
* Constructor for the MidiImport class.
* @param {string} _fpath - The file path of the MIDI file.
* @param {function} _f - The function for returning the nth Farey set.
* @param {number} _anc - The anacrusis value.
*/
function MidiImport(_fpath) {
var _f = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : mu.farey(4);
var _anc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
_classCallCheck(this, MidiImport);
// Workaround for JS context peculiarities.
// var self = this;
this.fpath = _fpath;
this.data = this.get_data(this.fpath);
this.timeSigs = this.get_time_sigs();
this.anacrusis = _anc;
this.points = this.get_points();
// this.points.slice(0, 3).forEach(function(p, i){
// console.log("points[" + i + "]:", p)
// })
this.controlChanges = this.get_control_changes();
this.compObj = this.get_comp_obj(_f);
// Possible to return something.
// return sth;
}
/**
* Finds the bass track in the MIDI file.
* @return {Array} candidates - The array of candidates for bass tracks.
*/
_createClass(MidiImport, [{
key: 'find_bass_track',
value: function find_bass_track() {
var tracks = this.data.tracks;
var trg = [["bass"]];
var candidates = [];
var synthCandidates = []; // In absence of bass family, accept this.
// First phase of finding.
tracks.forEach(function (trk, idx) {
var fam = trk.instrument.family;
var nam = trk.instrument.name;
trg.forEach(function (t) {
if (fam === t[0]) {
candidates.push([idx, fam + " -> " + nam]);
}
});
if (fam === "synth lead" && nam === "lead 8 (bass + lead)") {
synthCandidates.push([idx, "synth lead -> lead 8 (bass + lead)"]);
}
});
// Remove any empty tracks.
candidates = candidates.filter(function (c) {
return tracks[c[0]].notes.length > 0;
});
synthCandidates = synthCandidates.filter(function (c) {
return tracks[c[0]].notes.length > 0;
}).sort(function (a, b) {
return tracks[b[0]].notes.length - tracks[a[0]].notes.length;
});
if (candidates.length === 0) {
if (synthCandidates.length > 0) {
console.log("Found a suitable synth instead in absence of any bass.");
return [synthCandidates[0]];
}
console.log("No bass track targets identified. Returning undefined.");
tracks.forEach(function (trk, idx) {
var fam = trk.instrument.family;
var nam = trk.instrument.name;
console.log("fam:", fam, ", nam:", nam);
});
return;
}
if (candidates.length === 1) {
return candidates;
}
console.log("Multiple bass track targets identified. Returning them.");
console.log("candidates:", candidates);
candidates.forEach(function (c) {
var fam = tracks[c[0]].instrument.family;
var nam = tracks[c[0]].instrument.name;
console.log("fam:", fam, ", nam:", nam);
console.log("tracks[c[0]].notes.length:", tracks[c[0]].notes.length);
});
return candidates;
}
/**
* Finds the drum track in the MIDI file.
* @return {Array} candidates - The array of candidates for drum tracks.
*/
}, {
key: 'find_drum_track',
value: function find_drum_track() {
var tracks = this.data.tracks;
var trg = [["drums"]];
var candidates = [];
// First phase of finding.
tracks.forEach(function (trk, idx) {
var fam = trk.instrument.family;
var nam = trk.instrument.name;
trg.forEach(function (t) {
if (fam === t[0]) {
candidates.push([idx, fam + " -> " + nam]);
}
});
});
// Remove any empty tracks.
candidates = candidates.filter(function (c) {
return tracks[c[0]].notes.length > 0;
});
if (candidates.length === 0) {
console.log("No drum track targets identified. Returning undefined.");
tracks.forEach(function (trk, idx) {
var fam = trk.instrument.family;
var nam = trk.instrument.name;
console.log("fam:", fam, ", nam:", nam);
});
return;
}
if (candidates.length === 1) {
return candidates;
}
console.log("Multiple drum track targets identified. Returning them.");
console.log("candidates:", candidates);
candidates.forEach(function (c) {
var fam = tracks[c[0]].instrument.family;
var nam = tracks[c[0]].instrument.name;
console.log("fam:", fam, ", nam:", nam);
console.log("tracks[c[0]].notes.length:", tracks[c[0]].notes.length);
});
return candidates;
}
/**
* Finds the homophonic track in the MIDI file.
* @return {Array} candidates - The array of candidates for homophonic tracks.
*/
}, {
key: 'find_homophonic_track',
value: function find_homophonic_track() {
var self = this;
var tracks = self.data.tracks.filter(function (trk) {
return trk.instrument.family !== "drums" && trk.notes.length > 50;
});
var homophonyScores = tracks.map(function (trk, idx) {
var points = trk.notes.map(function (n) {
return [n.ticks / self.data.header.ppq, n.midi, null, n.durationTicks / self.data.header.ppq, tracks[idx].channel, Math.round(1000 * n.velocity) / 1000];
});
var seg = mu.segment(points, false);
var homoCount = 0;
seg.forEach(function (s) {
if (s.points.length >= 2) {
homoCount += s.points.length;
}
});
return { "idx": idx, "score": homoCount / seg.length };
}).sort(function (a, b) {
return b.score - a.score;
});
console.log("homophonyScores:", homophonyScores);
if (homophonyScores.length > 0 && homophonyScores[0]["score"] >= 2) {
var relIdx = homophonyScores[0]["idx"];
return [relIdx, tracks[relIdx].instrument.family + " -> " + tracks[relIdx].instrument.name, homophonyScores[0]["score"]];
}
}
/**
* Finds the vocal track in the MIDI file.
* @return {Array} candidates - The array of candidates for vocal tracks.
*/
}, {
key: 'find_vocal_track',
value: function find_vocal_track() {
var self = this;
var tracks = self.data.tracks;
var trg = [["reed", "alto sax"], ["synth lead", "lead 8 (bass + lead)"], ["reed", "clarinet"], ["reed", "soprano sax"], ["reed", "tenor sax"], ["reed", "baritone sax"], ["pipe", "flute"], ["brass", "french horn"], ["strings", "cello"], ["organ", "rock organ"], ["ensemble", "voice oohs"], ["guitar", "electric guitar (clean)"], ["guitar", "electric guitar (jazz)"], ["brass", "brass section"], ["reed", "oboe"], ["brass", "synthbrass 1"], ["guitar", "acoustic guitar (steel)"], ["synth pad", "pad 3 (polysynth)"], ["synth pad", "pad 4 (choir)"], ["piano", "acoustic grand piano"], ["strings", "viola"], ["brass", "muted trumpet"], ["ensemble", "synth voice"], ["strings", "violin"], ["guitar", "overdriven guitar"], ["guitar", "acoustic guitar (nylon)"], ["guitar", "distortion guitar"], ["guitar", "electric guitar (muted)"], ["piano", "bright acoustic piano"]];
var candidates = [];
// First phase of finding.
tracks.forEach(function (trk, idx) {
var fam = trk.instrument.family;
var nam = trk.instrument.name;
// console.log("fam:", fam, ", nam:", nam)
trg.forEach(function (t) {
if (fam === t[0]) {
if (fam === "synth lead") {
if (nam === t[1]) {
candidates.push([idx, fam + " -> " + nam]);
} else {
candidates.push([idx, "generic synth lead"]);
}
} else {
if (nam === t[1]) {
candidates.push([idx, fam + " -> " + nam]);
}
}
}
});
});
// Remove any empty tracks.
candidates = candidates.filter(function (c) {
return tracks[c[0]].notes.length > 0;
});
if (candidates.length === 0) {
console.log("No vocal track targets identified. Returning undefined.");
tracks.forEach(function (trk, idx) {
var fam = trk.instrument.family;
var nam = trk.instrument.name;
console.log("fam:", fam, ", nam:", nam);
});
return;
}
if (candidates.length === 1) {
return candidates[0];
}
// Second phase. Choosing between multiple piano tracks.
if (candidates.length > 1 && candidates.every(function (c) {
return c[1] === "piano -> acoustic grand piano";
})) {
// Sometimes there are multiple instances of piano -> acoustic grand piano.
// I'm going to search for and return the most monophonic of those.
console.log("candidates:", candidates);
var monophonyScores = candidates.map(function (c) {
var points = tracks[c[0]].notes.map(function (n) {
return [n.ticks / self.data.header.ppq, n.midi, null, n.durationTicks / self.data.header.ppq, tracks[c[0]].channel, Math.round(1000 * n.velocity) / 1000];
});
console.log("points.length:", points.length);
console.log("points.slice(0, 5):", points.slice(0, 5));
var seg = mu.segment(points, false);
var monoCount = 0;
seg.forEach(function (s) {
if (s.points.length < 2) {
monoCount++;
}
});
return monoCount / seg.length;
});
console.log("monophonyScores:", monophonyScores);
var ma = mu.max_argmax(monophonyScores);
return candidates[ma[1]];
}
// Third phase. Return first match in case of multiple matches.
var i = 0;
var relIdx = -1;
while (i < trg.length && relIdx < 0) {
relIdx = candidates.findIndex(function (c) {
return c[1] === trg[i][0] + " -> " + trg[i][1];
});
if (relIdx >= 0) {
i = trg.length - 1;
}
i++;
}
if (relIdx >= 0) {
return candidates[relIdx];
}
// Fourth phase of finding
// Sometimes there are multiple of synth lead, I'm returning the first
// encountered.
relIdx = candidates.findIndex(function (c) {
return c[1] === "generic synth lead";
});
if (relIdx >= 0) {
return candidates[relIdx];
}
console.log("\n!! Vocal track determination unclear !!");
tracks.forEach(function (trk, idx) {
var fam = trk.instrument.family;
var nam = trk.instrument.name;
console.log("fam:", fam, ", nam:", nam);
});
}
/**
* Gets the comp obj for the MIDI file.
* @param {function} f - The function for returning the nth Farey set.
* @return {object} comp - The comp obj for the MIDI file.
*/
}, {
key: 'get_comp_obj',
value: function get_comp_obj(f) {
// const fsm = mu.fifth_steps_mode(this.points, mu.krumhansl_and_kessler_key_profiles)
// console.log("fsm:", fsm)
// this.points.map(function(p){
// p.splice(2, 0, mu.guess_morphetic(p[1], fsm[2], fsm[3]))
// })
var an = new _Analyzer2.default();
var comp = an.note_point_set2comp_obj(this.points, this.timeSigs, false, f, 0, 1, 2, 3, 4);
console.log("comp.notes.length:", comp.notes.length);
// Control changes.
if (this.controlChanges !== undefined) {
if (comp.miscImport === undefined) {
comp.miscImport = {};
}
if (comp.miscImport.midi === undefined) {
comp.miscImport.midi = {};
}
comp.miscImport.midi.controlChange = this.controlChanges;
}
// Strip off file extension.
var pFile = pa.basename(this.fpath, pa.extname(this.fpath));
comp["id"] = pFile;
comp["name"] = pFile;
comp["composers"] = [{ "id": "default_composer", "name": "none", "displayName": "None" }];
return comp;
}
/**
* Gets the control changes for the MIDI file.
* @param {number} anacrusis - The anacrusis value.
* @return {Array} cc - The array of control changes for the MIDI file.
*/
}, {
key: 'get_control_changes',
value: function get_control_changes(anacrusis) {
var self = this;
var cc = self.data.tracks.map(function (track) {
var ccCurrTrack = {};
var props = Object.keys(track.controlChanges);
props.forEach(function (p) {
ccCurrTrack[p] = track.controlChanges[p].map(function (c) {
var obj = mu.timelapse_object();
obj.ontime = Math.round(100000 * (c.ticks / self.data.header.ppq - anacrusis)) / 100000;
// What about the anacrusis effect on onset?!
obj.onset = Math.round(100000 * c.time) / 100000;
obj.value = Math.round(100000 * c.value) / 100000;
return obj;
});
});
return ccCurrTrack;
});
return cc;
}
/**
* Gets the data from the MIDI file.
* @return {object} midiData - The data from the MIDI file.
*/
}, {
key: 'get_data',
value: function get_data() {
var midiData = fs.readFileSync(this.fpath);
return new Midi(midiData);
}
/**
* Gets the phrase boundary ontimes for the MIDI file.
* @param {number} [restDur=1] - The rest duration.
* @param {string} [property="offtime"] - The property of the phrase boundary.
* @return {Array} pbo - The array of phrase boundary ontimes for the MIDI file.
*/
}, {
key: 'get_phrase_boundary_ontimes',
value: function get_phrase_boundary_ontimes() {
var restDur = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
var property = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "offtime";
var pbo = [];
var segs = mu.segment(this.points, true, 0, 2);
segs.forEach(function (seg, idx) {
if (seg.points.length == 0 && seg.offtime - seg.ontime >= restDur) {
pbo.push(seg[property]);
// Fixed this bug where property wasn't being used...
// pbo.push(seg.offtime)
}
});
return pbo;
}
/**
* Get points from the MIDI file
*
* @param {number} anacrusis - Anacrusis, the unaccented beats at the beginning of a musical phrase
* @return {array} points - List of sorted points
*/
}, {
key: 'get_points',
value: function get_points() {
var self = this;
var points = [];
self.data.tracks.forEach(function (track, index) {
track.notes.forEach(function (n) {
points.push([n.ticks / self.data.header.ppq, n.midi, n.durationTicks / self.data.header.ppq, index, Math.round(1000 * n.velocity) / 1000]);
});
});
var unqPoints = mu.unique_rows(points, true)[0];
var minOntime = unqPoints[0][0];
if (this.anacrusis === "Shift to zero") {
unqPoints.forEach(function (pt) {
pt[0] -= minOntime;
});
} else if (this.anacrusis > 0) {
unqPoints.forEach(function (pt) {
pt[0] -= this.anacrusis;
});
}
// Update anacrusis so it is no longer a string value.
this.anacrusis = unqPoints[0][0];
return unqPoints;
// Old version that just used sort_rows(), which isn't good enough if the
// MIDI file happens to contain duplicate events.
// const sortedPoints = mu.sort_rows(points)[0]
// const minOntime = sortedPoints[0][0]
// if (this.anacrusis === "Shift to zero"){
// sortedPoints.forEach(function(pt){
// pt[0] -= sortedPoints[0][0]
// })
// }
// else if (anacrusis > 0){
// sortedPoints.forEach(function(pt){
// pt[0] -= anacrusis
// })
// }
// // Update anacrusis so it is no longer a string value.
// this.anacrusis = sortedPoints[0][0]
// return sortedPoints
}
/**
* Get time signatures from the MIDI file
*
* @return {array} timeSigs - List of time signatures with bar number, top number, bottom number, and ontime
*/
}, {
key: 'get_time_sigs',
value: function get_time_sigs() {
var self = this;
// Set defaul timeSigs.
var timeSigs = [{ "barNo": 1, "topNo": 4, "bottomNo": 4, "ontime": 0 }];
if (self.data.header && self.data.header.timeSignatures && self.data.header.timeSignatures.length > 0) {
// Some meaningful time signature data are present, so import them.
timeSigs = [self.data.header.timeSignatures.map(function (ts) {
return {
"barNo": ts.measures + 1,
"topNo": ts.timeSignature[0],
"bottomNo": ts.timeSignature[1],
"ontime": ts.ticks / self.data.header.ppq
};
})[0]];
}
console.log("timeSigs:", timeSigs);
return timeSigs;
}
}]);
return MidiImport;
}();
exports.default = MidiImport;