maia-util
Version:
Utility math and music functions supporting various applications by Music Artificial Intelligence Algorithms, Inc.
230 lines (219 loc) • 9.78 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = harman_forward;
var _find_segment_against_template = require('./find_segment_against_template');
var _find_segment_against_template2 = _interopRequireDefault(_find_segment_against_template);
var _unique_rows = require('./unique_rows');
var _unique_rows2 = _interopRequireDefault(_unique_rows);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* This function labels input sets of notes (in `segments`), with chord names as
* provided in `templates` and `lookup`. Contiguous sets of notes may be
* combined and given the same label, if it is parsimonious to do so according
* to the algorithm. There is also some tolerance for non-chord tones. The
* function is an implementation of the forwards HarmAn algorithm of Pardo and
* Birmingham (2002).
*
* @author Tom Collins
* @comment 26th October 2011
* @tutorial chord-labelling-1
* @param {Segment[]} segments - An array of segments
* @param {number[][]} templates - An array of pitch-class sets.
* @param {string[]} lookup - An array of strings paraellel to templates, giving
* the interpretable label of each pitch-class set.
* @return {Segment[]} An array of segments where the additional, extra
* properties of name, index, and score have been populated, so as to (possibly
* combine) and label the chords from the input `segments` with names from
* `templates` and `lookup`.
*
* @example
* var ps = [
* [0, 45, 4], [0.5, 52, 3.5], [1, 59, 0.5], [1.5, 60, 2.5],
* [4, 41, 4], [4.5, 48, 3.5], [5, 55, 0.5], [5.5, 57, 2.5]
* ];
* var seg = segment(ps);
* harman_forward(seg, chord_templates_pbmin7ths, chord_lookup_pbmin7ths);
* →
* [
* {
* "ontime": 0,
* "offtime": 4.5,
* "points": [
* [0, 45, 51, 4, 0],
* [0.5, 52, 55, 3.5, 0],
* [1, 59, 59, 0.5, 0],
* [1.5, 60, 60, 2.5, 0],
* [4, 41, 49, 4, 0]
* ],
* "name": "A minor",
* "index": 33,
* "score": 6
* },
* {
* "ontime": 4.5,
* "offtime": 8,
* "points": [
* [4, 41, 49, 4, 0],
* [4.5, 48, 53, 3.5, 0],
* [5, 55, 57, 0.5, 0],
* [5.5, 57, 58, 2.5, 0]
* ],
* "name": "F major",
* "index": 5,
* "score": 6
* }
* ]
*/
function harman_forward(segments, templates, lookup) {
var L = segments.length;
var lab = new Array();
// Get the score for the very first segment.
if (L > 0) {
currentfind = (0, _find_segment_against_template2.default)(segments[0], templates);
currentscore = currentfind.score;
}
// No testing to be done here, because there is no previous segment with
// which to combine it, so just add it to lab.
lab[0] = {
"ontime": segments[0].ontime, "offtime": segments[0].offtime,
"points": segments[0].points, "score": currentscore,
"index": currentfind.index, "name": lookup[currentfind.index]
};
// console.log('lab[0].points:', lab[0].points);
var i = 1; // Iterate over the remaining elements of segments.
var j = 1; // Iterate over entries to lab.
while (i < L) {
// Get the score from the last segment.
var lastscore = lab[j - 1].score;
// console.log('lastscore:', lastscore);
// Get the score from the current segment.
var currentfind = (0, _find_segment_against_template2.default)(segments[i], templates);
var currentscore = currentfind.score;
// console.log('currentscore:', currentscore);
// Get the score from combining the last and current segments.
// In my opinion (although I've not read the paper recently), the contents
// of these segments ought to be de-duplicated as they are combined, e.g.
// var duopoints = unique_rows(lab[j - 1].points.concat(segments[i].points));
// This seems to cause over-segmentation, however, so I'm not doing this for
// now. I am doing a final de-dupe before returning.
var duopoints = [lab[j - 1].points.concat(segments[i].points)];
var duosegments = {
"ontime": lab[j - 1].ontime,
"offtime": segments[i].offtime,
"points": duopoints[0]
};
// console.log('duosegments.points:', duosegments.points);
var combinedfind = (0, _find_segment_against_template2.default)(duosegments, templates);
var combinedscore = combinedfind.score;
// console.log('combinedscore:', combinedscore);
// The test!
if (combinedscore < lastscore + currentscore) {
// Label separately.
lab.push({
"ontime": segments[i].ontime, "offtime": segments[i].offtime,
"points": segments[i].points, "score": currentscore,
"index": currentfind.index, "name": lookup[currentfind.index]
});
j++;
} else {
// Label the combined segment.
lab.pop();
lab.push({
"ontime": duosegments.ontime, "offtime": duosegments.offtime,
"points": duosegments.points, "score": combinedscore,
"index": combinedfind.index, "name": lookup[combinedfind.index]
});
}
// console.log('lab[' + (j - 1) + '].points:', lab[j - 1].points);
i++;
}
return lab.map(function (s) {
// De-dupe members of the points property.
var psIdx = (0, _unique_rows2.default)(s.points);
s.points = psIdx[0];
return s;
});
}
// Informal testing
// import segment from './segment'
// var chord_templates_pbmin7ths = [
// // Major triads
// [0, 4, 7], [1, 5 ,8], [2, 6, 9], [3, 7, 10], [4, 8, 11], [5 ,9, 0],
// [6, 10, 1], [7, 11, 2], [8, 0, 3], [9, 1, 4], [10, 2, 5], [11, 3, 6],
// // Dominant 7th triads
// [0, 4, 7, 10], [1, 5 ,8, 11], [2, 6, 9, 0], [3, 7, 10, 1],
// [4, 8, 11, 2], [5 ,9, 0, 3], [6, 10, 1, 4], [7, 11, 2, 5],
// [8, 0, 3, 6], [9, 1, 4, 7], [10, 2, 5 ,8], [11, 3, 6, 9],
// // Minor triads
// [0, 3, 7], [1, 4, 8], [2, 5 ,9], [3, 6, 10], [4, 7, 11], [5 ,8, 0],
// [6, 9, 1], [7, 10, 2], [8, 11, 3], [9, 0, 4], [10, 1, 5], [11, 2, 6],
// // Fully diminished 7th
// [0, 3, 6, 9], [1, 4, 7, 10], [2, 5 ,8, 11],
// // Half diminished 7th
// [0, 3, 6, 10], [1, 4, 7, 11], [2, 5 ,8, 0], [3, 6, 9, 1],
// [4, 7, 10, 2], [5 ,8, 11, 3], [6, 9, 0, 4], [7, 10, 1, 5],
// [8, 11, 2, 6], [9, 0, 3, 7], [10, 1, 4, 8], [11, 2, 5 ,9],
// // Diminished triad
// [0, 3, 6], [1, 4, 7], [2, 5 ,8], [3, 6, 9], [4, 7, 10], [5 ,8, 11],
// [6, 9, 0], [7, 10, 1], [8, 11, 2], [9, 0, 3], [10, 1, 4],
// [11, 2, 5],
// // Minor 7th
// [0, 3, 7, 10], [1, 4, 8, 11], [2, 5 ,9, 0], [3, 6, 10, 1],
// [4, 7, 11, 2], [5 ,8, 0, 3], [6, 9, 1, 4], [7, 10, 2, 5],
// [8, 11, 3, 6], [9, 0, 4, 7], [10, 1, 5 ,8], [11, 2, 6, 9]
// ];
//
// var chord_lookup_pbmin7ths = [
// "C major", "Db major", "D major", "Eb major", "E major", "F major",
// "F# major", "G major", "Ab major", "A major", "Bb major", "B major",
// "C 7", "Db 7", "D 7", "Eb 7", "E 7", "F 7",
// "F# 7", "G 7", "Ab 7", "A 7", "Bb 7", "B 7",
// "C minor", "Db minor", "D minor", "Eb minor", "E minor", "F minor",
// "F# minor", "G minor", "Ab minor", "A minor", "Bb minor", "B minor",
// // Because Pardo & Birmingham (2002) only use MIDI note,
// // there is a bit of an issue with diminished 7th chords
// // (next three labels), as you can't tell for instance
// // whether the pitch classes 0, 3, 6, 9 are C Dim 7,
// // D# Dim 7, F# Dim 7, or A Dim 7. In my Lisp
// // implementation, I use the surrounding musical context
// // (including pitch names derived from the combination of
// // MIDI and morphetic pitch numbers) to attempt to resolve
// // any ambiguities, but in this JavaScript implementation, I
// // just assume it's F# Dim 7 (or G Dim 7 or "G# Dim 7
// // respectively).
// "F# Dim 7", "G Dim 7", "G# Dim 7",
// "C half dim 7", "Db half dim 7", "D half dim 7", "Eb half dim 7", "E half dim 7", "F half dim 7",
// "F# half dim 7", "G half dim 7", "Ab half dim 7", "A half dim 7", "Bb half dim 7", "B half dim 7",
// "C dim", "Db dim", "D dim", "Eb dim", "E dim", "F dim",
// "F# dim", "G dim", "Ab dim", "A dim", "Bb dim", "B dim",
// "C minor 7", "Db minor 7", "D minor 7", "Eb minor 7", "E minor 7", "F minor 7",
// "F# minor 7", "G minor 7", "Ab minor 7", "A minor 7", "Bb minor 7", "B minor 7"
// ];
// var ps = [
// [0, 45, 51, 4, 0], [0, 72, 67, 4, 1], [0, 76, 69, 4, 1], [0.5, 52, 55, 3.5, 0], [1, 59, 59, 0.5, 0], [1.5, 60, 60, 2.5, 0],
// [4, 41, 49, 4, 0], [4, 72, 67, 4, 1], [4, 77, 70, 4, 1], [4.5, 48, 53, 3.5, 0], [5, 55, 57, 0.5, 0], [5.5, 57, 58, 2.5, 0]
// ];
// var seg = segment(ps);
// console.log('seg:', seg);
// var lbl = harman_forward(
// seg,
// chord_templates_pbmin7ths,
// chord_lookup_pbmin7ths
// );
// console.log("lbl:", JSON.stringify(lbl));
// var ps = [
// [-1, 74, 68, 0.75, 0], [-0.25, 71, 66, 0.25, 0],
// [0, 55, 57, 0.5, 1], [0, 74, 68, 1, 0], [0.5, 59, 59, 0.5, 1], [1, 62, 61, 0.5, 1], [1.5, 59, 59, 0.5, 1], [2, 62, 61, 0.5, 1], [2, 67, 64, 1, 0], [2.5, 59, 59, 0.5, 1],
// [3, 57, 58, 0.5, 1], [3, 66, 63, 1, 0], [3.5, 60, 60, 0.5, 1], [4, 62, 61, 0.5, 1], [4.5, 60, 60, 0.5, 1], [5, 62, 61, 0.5, 1], [5, 81, 72, 0.75, 0], [5.5, 60, 60, 0.5, 1], [5.75, 78, 70, 0.25, 0],
// [6, 54, 56, 0.5, 1], [6, 81, 72, 1, 0], [6.5, 57, 58, 0.5, 1], [7, 62, 61, 0.5, 1], [7.5, 57, 58, 0.5, 1], [8, 62, 61, 0.5, 1], [8, 72, 67, 1, 0], [8.5, 57, 58, 0.5, 1],
// [9, 55, 57, 0.5, 1], [9, 71, 66, 1, 0], [9.5, 59, 59, 0.5, 1], [10, 62, 61, 0.5, 1], [10.5, 59, 59, 0.5, 1]
// ];
// var seg = segment(ps);
// var lbl = harman_forward(
// seg,
// chord_templates_pbmin7ths,
// chord_lookup_pbmin7ths
// );
// console.log('lbl:', JSON.stringify(lbl));