UNPKG

maia-markov

Version:

Markov analysis and generation functions supporting various applications by Music Artificial Intelligence Algorithms, Inc.

535 lines (490 loc) 21.8 kB
'use strict'; 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); var _MidiExport = require('./MidiExport'); var _MidiExport2 = _interopRequireDefault(_MidiExport); 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 path = require('path'); var _require = require('@tonejs/midi'), Midi = _require.Midi; var mu = require('maia-util'); var an = new _Analyzer2.default(); /** * Class for importing MIDI files and extracting information from them. */ var MelodyExtractor = 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 MelodyExtractor(_fpath) { var _param = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { "indices": { "ontime": 0, "mnn": 1, "duration": 2, "channel": 3, "velocity": 4 }, // "quantisationSet": [0, 1/6, 1/4, 1/3, 1/2, 2/3, 3/4, 5/6, 1], // "anacrusis": 0, "pitchModulo": 12, "winSize": 2, "stepSize": 1, "velMnnWeight": 0.5 }; _classCallCheck(this, MelodyExtractor); // Workaround for JS context peculiarities. // var self = this; this.fpath = _fpath; this.fname = path.basename(this.fpath); this.ontimeIndex = _param["indices"]["ontime"]; this.mnnIndex = _param["indices"]["mnn"]; this.durationIndex = _param["indices"]["duration"]; this.chanIdx = _param["indices"]["channel"]; this.velIndex = _param["indices"]["velocity"]; this.modulo = _param["pitchModulo"]; this.winSize = _param["winSize"]; this.winStep = _param["stepSize"]; this.velMnnWeight = _param["velMnnWeight"]; this.points = this.get_tonal_points(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(MelodyExtractor, [{ key: 'extract_melody', value: function extract_melody() { var midiSaveDir = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; var self = this; try { var seg = mu.segment(self.points, true, self.ontimeIndex, self.durIndex); // Have a look at the first five segments. // console.log("seg.slice(0, 5):", seg.slice(0, 5)) var prominentNotes = seg.map(function (s) { var weightedCounts = []; for (var i = 0; i < self.modulo; i++) { weightedCounts[i] = { weightedCount: 0, origins: [] }; } s.points.forEach(function (p) { var mnn12 = p[self.mnnIndex] % self.modulo; weightedCounts[mnn12]["weightedCount"] += self.velMnnWeight * p[self.velIndex] + (1 - self.velMnnWeight) * (p[self.mnnIndex] / 127); weightedCounts[mnn12]["origins"].push(p); }); var finalCounts = weightedCounts.map(function (wc) { return wc.weightedCount; }); // const winner = mu.max_argmax(finalCounts) // We only return the first note with maximun strength at present. // If more than one note contributes to the maximum strength, we should save all of them. var winner = self.return_max(finalCounts); // console.log("winner", winner.slice(0, 5)) // Winner[1] will tell us which pitch contribute the winner, and we could use it to find the origin channel // const relevantOrigins = weightedCounts[winner[1]]["origins"] // TODO: add instName and channel in line 106 // console.log("weightedCounts[winner[1]]['origins'][0]", weightedCounts[winner[1]]["origins"][0]) var currentOriginNote = weightedCounts[winner[0][1]]["origins"][0]; return { weightedCounts: weightedCounts, winner: winner, ontime: s.ontime, offtime: s.offtime, origins: currentOriginNote }; }); // console.log("prominentNotes", prominentNotes.slice(0,5)) // Reconstruct notes from the octave-free notes. // const transposedNotes = prominentNotes.map(function(info){ // return [ info.ontime, info.winner[1] + 72 ,0 , info.offtime - info.ontime, 0, 0.8 ] // }) // Reconstruct notes from the original notes, which always consider the first note with the maximum strength to be the melody note. // let transposedNotes = [] // prominentNotes.forEach(function(info){ // const currentNote = info.origins // if(currentNote !== undefined){ // const originalNoteArr = [ currentNote[0], currentNote[1] ,0 , currentNote[2], currentNote[3], currentNote[4] ] // // Avoid duplicate note. // if(transposedNotes.indexOf(originalNoteArr) === -1){ // transposedNotes.push(originalNoteArr) // } // } // }) var transposedNotes = self.reconstruct_melody_witn_window(prominentNotes); // console.log("transposedNotes", transposedNotes.slice(0,5)) // Export MIDI file. if (midiSaveDir) { var _fpath = path.join(midiSaveDir, self.fname); new _MidiExport2.default(transposedNotes, [], _fpath, { "scaleFactor": 0.5, "timeSigTopNo": 4, "timeSigBottomNo": 4, "noteIndices": { "ontimeIndex": 0, "mnnIndex": 1, "durationIndex": 3, "channelIndex": 4, "velocityIndex": 5 }, "ccIndices": { "ontimeIndex": 0, "numberIndex": 1, "channelIndex": 2, "valueIndex": 3 } }); } return transposedNotes; } catch (e) { console.log(e); } } }, { key: 'get_tonal_points', value: function get_tonal_points(midiPath) { var midiData = fs.readFileSync(midiPath); var midi = new Midi(midiData); // Grab time signature information. var timeSigs = [midi.header.timeSignatures.map(function (ts) { return { "barNo": ts.measures + 1, "topNo": ts.timeSignature[0], "bottomNo": ts.timeSignature[1], "ontime": ts.ticks / midi.header.ppq }; })[0]]; // Here we will get the track/instrument information. // console.log("Track information:", midi.tracks) var allTracks = []; // In future if you want to separate out percussion tracks, something like // track.instrument.family === "drums" // and defining a second or third array above to hold separated tracks // might be a good idea. var tmp_track_cnt = 0; var max_tick = 0; var tmp_instrument = []; var tonalPoints = []; midi.tracks.forEach(function (track, idx) { var allPoints = []; if (track.notes.length > 0) { tmp_track_cnt++; var instrInfo = track.instrument.family + " -> " + track.instrument.name; // console.log("instrInfo:", instrInfo) // // Get instrument number // console.log("track.instrument.number:", track.instrument.number) tmp_instrument.push({ "number": track.instrument.number, "family": track.instrument.family, "name": track.instrument.name }); track.notes.forEach(function (n) { // Update max_tick: if (n.ticks + n.durationTicks > max_tick) { max_tick = n.ticks + n.durationTicks; } // pt = [beat, midi note number, duration, channel, velocity] if (n.midi <= 127 && n.midi >= 0) { var pt = [Math.round(100000 * (n.ticks / midi.header.ppq)) / 100000, n.midi, Math.round(100000 * (n.durationTicks / midi.header.ppq)) / 100000, track.channel, Math.round(1000 * n.velocity) / 1000]; allPoints.push(pt); if (track.instrument.family !== "drums") { // Tom uses maia-util uniqueness code below. tonalPoints.push(pt); // Chenyu's code // if(tonalPoints.indexOf(pt) === -1){ // tonalPoints.push(pt) // } } } }); if (allPoints.length > 0) { allTracks.push({ "pt": allPoints, "instrument": track.instrument, "name": track.name, "channel": track.channel, "tempo": midi.header.tempos }); } } // console.log("midi.header", midi.header) }); // Tom's code for sorting points. tonalPoints = mu.unique_rows(tonalPoints, true)[0]; // Chenyu's code // tonalPoints = tonalPoints.sort(function(a, b){ // return a[0]-b[0] // }) return tonalPoints; } }, { key: 'find_note_idx_in_window', value: function find_note_idx_in_window(prominentNotes, startOntime, endOntime) { var startIdx = 0; var endIdx = 0; for (var i = 1; i <= prominentNotes.length - 1; i++) { var currentProminentNote = prominentNotes[i]; if (currentProminentNote.offtime > startOntime && prominentNotes[i - 1].offtime <= startOntime && prominentNotes[i - 1].ontime < endOntime) { startIdx = i - 1; } if (currentProminentNote.ontime >= endOntime && prominentNotes[i - 1].ontime < endOntime && prominentNotes[i - 1].ontime > startOntime) { endIdx = i - 1; } } return [startIdx, endIdx]; } }, { key: 'find_note_idx_in_window_specific_channel', value: function find_note_idx_in_window_specific_channel(channel, tmpWinStart, tmpWinEnd) { // console.log("******channel", channel) var flag = 0; for (var i = 0; i <= this.points.length - 1; i++) { if (this.points[i][3] === channel && this.points[i][0] >= tmpWinStart && this.points[i][0] < tmpWinEnd) { flag = 1; // console.log("orgPoints", this.points[i]) } } return flag; } }, { key: 'reconstruct_melody_witn_window', value: function reconstruct_melody_witn_window(prominentNotes) { var _this = this; var self = this; var melodyChannel = []; var winSize = this.winSize; var winStep = this.winStep; var timeSig = 4; // TODO: we will need to know the real time segment. var winStart = 0; var winEnd = void 0; if (this.winSize === null) { winEnd = prominentNotes[prominentNotes.length - 1].ontime + 1; } else { winEnd = winStart + winSize * timeSig; } // console.log("winStart:", winStart) // console.log("winEnd:", winEnd) var winEndOntime = prominentNotes[prominentNotes.length - 1].ontime; // console.log("prominentNotes.length", prominentNotes.length) // Notes in weightedCounts [ ontime, pitch, duration, channel, velocity] while (winStart < winEndOntime) { var winIdx = this.find_note_idx_in_window(prominentNotes, winStart, winEnd); if (this.winSize === null) { winIdx[1] = prominentNotes.length - 1; } // console.log("winIdx:", winIdx) // console.log("***winStart", winStart) // console.log("***winEnd", winEnd) if (winIdx[1] !== 0) { (function () { var findMelodyOut = _this.find_melody_channel(prominentNotes, winIdx[0], winIdx[1]); var currentMelodyChannel = findMelodyOut[0]; var currentMaxStrength = findMelodyOut[1]; // console.log("currentMelodyChannel", currentMelodyChannel) // Update the array that stores the position and channel for melody. // Need to check if current measure is empty, if so, start from winStart + 1 bar. currentMelodyChannel.forEach(function (item) { for (var tmpWinStart = winStart; tmpWinStart + 1 * timeSig <= winEnd; tmpWinStart = tmpWinStart + 1 * timeSig) { var tmpWinEnd = tmpWinStart + 1 * timeSig; // console.log("&&&&&&&tmpWinStart", tmpWinStart) // console.log("&&&&&&&tmpWinEnd", tmpWinEnd) var flag = self.find_note_idx_in_window_specific_channel(parseInt(item), tmpWinStart, tmpWinEnd); if (flag !== 0) { melodyChannel.push([parseInt(item), [tmpWinStart, tmpWinEnd], currentMaxStrength]); } } }); })(); } if (this.winSize === null) { winStart = winEndOntime; winEnd = null; } else { winStart = winStart + winStep * timeSig; winEnd = winStart + winSize * timeSig; } // console.log("nextStartIdx", nextStartIdx) } console.log("melodyChannel", melodyChannel); // Reconstruct melody from the full point set. var melodyNotes = this.get_melody_points(melodyChannel); return melodyNotes; } }, { key: 'get_melody_points', value: function get_melody_points(melodyChannel) { // Using the greedy algorithm to calculate which track should each measure belong to. var melodyPoints = []; var processedMelodyChannel = []; var startOntime = 0; var endOntime = melodyChannel[melodyChannel.length - 1][1][1]; while (startOntime < endOntime) { var channelCount = {}; for (var i = 0; i < melodyChannel.length; i++) { if (endOntime < startOntime) { break; } if (melodyChannel[i][1][0] > startOntime) { break; } if (melodyChannel[i][1][0] <= startOntime && melodyChannel[i][1][1] > startOntime) { var currentChannel = melodyChannel[i][0].toString(); if (!(currentChannel in channelCount)) { channelCount[currentChannel] = 0; } // channelCount[currentChannel] = channelCount[currentChannel] + 1 channelCount[currentChannel] += melodyChannel[i][2]; } } // Update processedMelodyChannel var maxCount = 0; var channelList = Object.keys(channelCount); var winChannel = channelList[0]; for (var _i = 0; _i < channelList.length; _i++) { if (channelCount[channelList[_i]] > maxCount) { maxCount = channelCount[channelList[_i]]; winChannel = channelList[_i]; } } processedMelodyChannel.push([parseInt(winChannel), startOntime]); startOntime = startOntime + 1; } // console.log("processedMelodyChannel", processedMelodyChannel) // Get melody points var currentOntimeIdx = 0; this.points.forEach(function (point) { // const originalNoteArr = [ currentNote[0], currentNote[1] ,0 , currentNote[2], currentNote[3], currentNote[4] ] if (point[0] >= processedMelodyChannel[currentOntimeIdx][1] + 1 && currentOntimeIdx < processedMelodyChannel.length - 1) { currentOntimeIdx = currentOntimeIdx + 1; } if (point[3] === processedMelodyChannel[currentOntimeIdx][0]) { melodyPoints.push([point[0], point[1], 0, point[2], point[3], point[4]]); } }); return melodyPoints; } }, { key: 'find_melody_channel', value: function find_melody_channel(prominentNotes, startIdx, endIdx) { var self = this; var strengthSum = {}; var orgPointsList = {}; var _loop = function _loop(i) { var currentWinner = prominentNotes[i]['winner']; // console.log("======currentWinner", currentWinner) currentWinner.forEach(function (winner) { // console.log("winner", winner) // console.log("prominentNotes[i]['weightedCounts'][currentWinner[1]]", prominentNotes[i]['weightedCounts']) var orgPoints = prominentNotes[i]['weightedCounts'][winner[1]]['origins'][0]; if (orgPoints !== undefined) { orgPoints = [orgPoints]; // console.log("orgPoints", orgPoints) orgPoints.forEach(function (point) { var currentChannel = point[self.chanIdx].toString(); if (!(currentChannel in orgPointsList)) { orgPointsList[currentChannel] = []; // console.log("orgPointsList", orgPointsList) } else if (orgPointsList[currentChannel].indexOf(point) === -1) { // console.log("point", point) orgPointsList[currentChannel].push(point); if (!(currentChannel in strengthSum)) { strengthSum[currentChannel] = 0; } // strengthSum[currentChannel] += point[self.velIndex] strengthSum[currentChannel] += prominentNotes[i]['weightedCounts'][winner[1]]['weightedCount']; } }); } }); }; for (var i = startIdx; i <= endIdx; i++) { _loop(i); } // console.log("orgPointsList", orgPointsList) // Calculate entropy for each channel. var entropyChannel = []; var channelSet = Object.keys(strengthSum); for (var i = 0; i < channelSet.length; i++) { entropyChannel[channelSet[i]] = 0; if (orgPointsList[channelSet[i]].length > 1) { (function () { var comp = an.note_point_set2comp_obj(orgPointsList[channelSet[i]], [{ "barNo": 1, "topNo": 4, "bottomNo": 4, "ontime": 0 }], false, null, 0, 1, 2, 3, 4); var relNotes = comp.notes; // if (idx === 0){ // console.log("Here are the first few notes on " + c.family + ", " + c.name) // console.log("relNotes.slice(0, 10):", relNotes.slice(0, 10)) // } // Get the beatOn and MNN properties in a numeric array. var arr = relNotes.map(function (n) { return [n.beatOn, n.MNN]; }); var hist = mu.count_rows(arr, undefined, true); var sumArr = mu.array_sum(hist[1]); // Convert count to probability distribution. var pdist = hist[1].map(function (freq) { return freq / sumArr; }); entropyChannel[channelSet[i]] = mu.entropy(pdist); // console.log("channelSet[i]", channelSet[i]) // console.log("entropyChannel[channelSet[i]]", entropyChannel[channelSet[i]]) })(); } } // Normalise noteStrength and entropyList. // For note strength var maxStrength = 0; for (var _i2 = 0; _i2 < channelSet.length; _i2++) { if (strengthSum[channelSet[_i2]] > maxStrength) { maxStrength = strengthSum[channelSet[_i2]]; } } for (var _i3 = 0; _i3 < channelSet.length; _i3++) { strengthSum[channelSet[_i3]] = strengthSum[channelSet[_i3]] / maxStrength; } // For entropy var maxEntropy = 0; for (var _i4 = 0; _i4 < channelSet.length; _i4++) { if (entropyChannel[channelSet[_i4]] > maxEntropy) { maxEntropy = entropyChannel[channelSet[_i4]]; } } for (var _i5 = 0; _i5 < channelSet.length; _i5++) { entropyChannel[channelSet[_i5]] = entropyChannel[channelSet[_i5]] / maxEntropy; } // Calculate strength+entropy var maxStrengthEntropy = 0; // console.log("strengthSum", strengthSum) // Let's return all melody channels with the maximum strength. var melodyChannel = []; for (var _i6 = 0; _i6 < channelSet.length; _i6++) { if (strengthSum[channelSet[_i6]] > maxStrengthEntropy) { maxStrengthEntropy = strengthSum[channelSet[_i6]] + entropyChannel[channelSet[_i6]]; } } for (var _i7 = 0; _i7 < channelSet.length; _i7++) { if (strengthSum[channelSet[_i7]] + entropyChannel[channelSet[_i7]] === maxStrengthEntropy) { melodyChannel.push(channelSet[_i7]); } } // console.log("melodyChannel", melodyChannel) return [melodyChannel, maxStrengthEntropy]; } }, { key: 'return_max', value: function return_max(finalCounts) { var outArr = []; var maxStrength = mu.max_argmax(finalCounts)[0]; finalCounts.forEach(function (strength, idx) { if (strength === maxStrength) { outArr.push([strength, idx]); } }); return outArr; } }]); return MelodyExtractor; }(); exports.default = MelodyExtractor; // ...