maia-markov
Version:
Markov analysis and generation functions supporting various applications by Music Artificial Intelligence Algorithms, Inc.
553 lines (480 loc) • 18.9 kB
JavaScript
// Imports
// import fs
const fs = require('fs')
const path = require('path')
const { Midi } = require('@tonejs/midi')
const mu = require('maia-util')
import Analyzer from './Analyzer'
const an = new Analyzer()
import MidiExport from './MidiExport'
/**
* Class for importing MIDI files and extracting information from them.
*/
class MelodyExtractor {
/**
* 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.
*/
constructor(
_fpath,
_param = {
"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
}
){
// 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.
*/
extract_melody(midiSaveDir = null){
const self = this
try {
const 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))
const prominentNotes = seg.map(function(s){
const weightedCounts = []
for (let i = 0; i < self.modulo; i++){
weightedCounts[i] = { weightedCount: 0, origins: [] }
}
s.points.forEach(function(p){
const 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)
})
const 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.
const 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])
const 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)
// }
// }
// })
const transposedNotes = self.reconstruct_melody_witn_window(prominentNotes)
// console.log("transposedNotes", transposedNotes.slice(0,5))
// Export MIDI file.
if (midiSaveDir){
const _fpath = path.join(midiSaveDir, self.fname)
new MidiExport(
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)
}
}
get_tonal_points(midiPath){
const midiData = fs.readFileSync(
midiPath
)
const midi = new Midi(midiData)
// Grab time signature information.
const 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)
let 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.
let tmp_track_cnt = 0
let max_tick = 0
let tmp_instrument = []
let tonalPoints = []
midi.tracks.forEach(function(track, idx){
let allPoints = []
if(track.notes.length > 0){
tmp_track_cnt ++
const 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){
let 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
}
find_note_idx_in_window(prominentNotes, startOntime, endOntime){
let startIdx = 0
let endIdx = 0
for(let i = 1; i <= prominentNotes.length-1; i ++){
const 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]
}
find_note_idx_in_window_specific_channel(channel, tmpWinStart, tmpWinEnd){
// console.log("******channel", channel)
let flag = 0
for(let 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
}
reconstruct_melody_witn_window(prominentNotes){
const self = this
let melodyChannel = []
const winSize = this.winSize
const winStep = this.winStep
const timeSig = 4 // TODO: we will need to know the real time segment.
let winStart = 0
let winEnd
if (this.winSize === null){
winEnd = prominentNotes[prominentNotes.length-1].ontime + 1
}
else {
winEnd = winStart + winSize*timeSig
}
// console.log("winStart:", winStart)
// console.log("winEnd:", winEnd)
const winEndOntime = prominentNotes[prominentNotes.length-1].ontime
// console.log("prominentNotes.length", prominentNotes.length)
// Notes in weightedCounts [ ontime, pitch, duration, channel, velocity]
while(winStart < winEndOntime){
const 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){
const findMelodyOut = this.find_melody_channel(prominentNotes, winIdx[0], winIdx[1])
const currentMelodyChannel = findMelodyOut[0]
const 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(let tmpWinStart = winStart; tmpWinStart + 1*timeSig <= winEnd; tmpWinStart = tmpWinStart+1*timeSig){
let tmpWinEnd = tmpWinStart + 1*timeSig
// console.log("&&&&&&&tmpWinStart", tmpWinStart)
// console.log("&&&&&&&tmpWinEnd", tmpWinEnd)
let 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.
const melodyNotes = this.get_melody_points(melodyChannel)
return melodyNotes
}
get_melody_points(melodyChannel){
// Using the greedy algorithm to calculate which track should each measure belong to.
let melodyPoints = []
let processedMelodyChannel = []
let startOntime = 0
let endOntime = melodyChannel[melodyChannel.length - 1][1][1]
while(startOntime < endOntime){
let channelCount = {}
for(let 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){
let currentChannel = melodyChannel[i][0].toString()
if(!(currentChannel in channelCount)){
channelCount[currentChannel] = 0
}
// channelCount[currentChannel] = channelCount[currentChannel] + 1
channelCount[currentChannel] += melodyChannel[i][2]
}
}
// Update processedMelodyChannel
let maxCount = 0
let channelList = Object.keys(channelCount)
let winChannel = channelList[0]
for(let 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
let 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
}
find_melody_channel(prominentNotes, startIdx, endIdx){
const self = this
let strengthSum = {}
let orgPointsList = {}
for(let i = startIdx; i <= endIdx; i ++){
let 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'])
let orgPoints = prominentNotes[i]['weightedCounts'][winner[1]]['origins'][0]
if(orgPoints !== undefined){
orgPoints = [orgPoints]
// console.log("orgPoints", orgPoints)
orgPoints.forEach(function(point){
const 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']
}
})
}
})
}
// console.log("orgPointsList", orgPointsList)
// Calculate entropy for each channel.
let entropyChannel = []
let channelSet = Object.keys(strengthSum)
for(let i = 0; i < channelSet.length; i ++){
entropyChannel[channelSet[i]] = 0
if(orgPointsList[channelSet[i]].length > 1){
const comp = an.note_point_set2comp_obj(orgPointsList[channelSet[i]],
[{"barNo": 1, "topNo": 4, "bottomNo": 4, "ontime": 0}],
false,
null,
0, 1, 2, 3, 4)
const 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.
const arr = relNotes.map(function(n){ return [n.beatOn, n.MNN] })
const hist = mu.count_rows(arr, undefined, true)
const sumArr = mu.array_sum(hist[1])
// Convert count to probability distribution.
const 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
let maxStrength = 0
for(let i = 0; i < channelSet.length; i ++){
if(strengthSum[channelSet[i]] > maxStrength){
maxStrength = strengthSum[channelSet[i]]
}
}
for(let i = 0; i < channelSet.length; i ++){
strengthSum[channelSet[i]] = strengthSum[channelSet[i]]/maxStrength
}
// For entropy
let maxEntropy = 0
for(let i = 0; i < channelSet.length; i ++){
if(entropyChannel[channelSet[i]] > maxEntropy){
maxEntropy = entropyChannel[channelSet[i]]
}
}
for(let i = 0; i < channelSet.length; i ++){
entropyChannel[channelSet[i]] = entropyChannel[channelSet[i]]/maxEntropy
}
// Calculate strength+entropy
let maxStrengthEntropy = 0
// console.log("strengthSum", strengthSum)
// Let's return all melody channels with the maximum strength.
let melodyChannel = []
for(let i = 0; i < channelSet.length; i ++){
if(strengthSum[channelSet[i]] > maxStrengthEntropy){
maxStrengthEntropy = strengthSum[channelSet[i]] + entropyChannel[channelSet[i]]
}
}
for(let i = 0; i < channelSet.length; i ++){
if(strengthSum[channelSet[i]] + entropyChannel[channelSet[i]] === maxStrengthEntropy){
melodyChannel.push(channelSet[i])
}
}
// console.log("melodyChannel", melodyChannel)
return [melodyChannel, maxStrengthEntropy]
}
return_max(finalCounts){
let outArr = []
let maxStrength = mu.max_argmax(finalCounts)[0]
finalCounts.forEach(function(strength, idx){
if(strength === maxStrength){
outArr.push([strength, idx])
}
})
return outArr
}
}
export default MelodyExtractor
// ...