maia-markov
Version:
Markov analysis and generation functions supporting various applications by Music Artificial Intelligence Algorithms, Inc.
495 lines (463 loc) • 19.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = Generator;
// Imports
var mu = require('maia-util');
// import 'maia-util'
// import mu from 'maia-util'
// Constructor for Generator object
function Generator() {}
// Workaround for JS context peculiarities.
// var self = this;
// Possible to return something.
// return sth;
// Methods for Generator object
Generator.prototype = {
constructor: Generator,
// Tom Collins 6/4/2016.
// Defining a modulo function because by default the modulus of a negative
// number in javascript is negative.
mod: function mod(a, n) {
return a - n * Math.floor(a / n);
},
get_lyrics_from_states: function get_lyrics_from_states(stateContextPairs, param) {
var stateType = param.stateType;
// Make a fresh copy because I was getting some weird-ass problems with idx.
var scp = JSON.parse(JSON.stringify(stateContextPairs));
// console.log("scp:", scp)
// Unpack states into a string.
console.log("scp[0]:", scp[0]);
var lyrics = "";
scp.forEach(function (s) {
lyrics += s[stateType][0] + " ";
});
lyrics += scp[scp.length - 1][stateType][1];
// lyrics = lyrics.slice(0, -1)
// Too clever, doesn't work!
// const lyrics = scp.reduce(function(total, s){
// return total + " " + s[stateType][0]
// })
// console.log("lyrics", lyrics)
return lyrics;
},
// Tom Collins 6/4/2016.
// This function converts beat-relative-MNN states into points consisting of
// ontimes, MNNs, MPNs, durations, and staff numbers.
get_points_from_states: function get_points_from_states(stateContextPairs, param) {
var self = this;
var stateType = param.stateType;
var pointReconstruction = param.pointReconstruction;
var currTimeSig = param.timeSignatures[0];
var crotchetBeatsInBar = 4 * currTimeSig.topNo / currTimeSig.bottomNo;
var idxOn = param.indices.ontime;
var idxMNN = param.indices.MNN;
var idxMPN = param.indices.MPN;
var idxDur = param.indices.duration;
var idxChan = param.indices.channel;
var idxVel = param.indices.velocity;
// stateContextPairs, stateType = "beat_rel_sq_MNN_state",
// currentTimeSignature = { "topNo": 4, "bottomNo": 4 }
// const idxOn = 0, idxMNN = 1, idxMPN = 2, idxDur = 3, idxChan = 4, idxVel = 5
// var crotchetBeatsInBar = 4*currentTimeSignature.topNo/currentTimeSignature.bottomNo;
// Make a fresh copy because I was getting some weird-ass problems with idx.
var scp = JSON.parse(JSON.stringify(stateContextPairs));
// console.log("scp:", scp)
// Unpack states into MNNs and MPNs.
scp.forEach(function (s) {
var MNNs = [];
var MPNs = [];
s.context.orig_points.forEach(function (p) {
var mnnMpnPair = self.state_representation_of_pitch([p[idxMNN], p[idxMPN]], param, s.context.tonic_pitch_closest);
MNNs.push(mnnMpnPair[0]);
MPNs.push(mnnMpnPair[1]);
});
s.MNNs = MNNs;
s.MPNs = MPNs;
});
// Get the ontimes for each state.
var ons = self.state_ontimes(scp, stateType, crotchetBeatsInBar);
scp.map(function (s, idx) {
s.ontime = ons[idx];
});
// Dovetail durations.
scp = self.dovetail_durations(scp, param);
// console.log("scp:", scp)
// Define points.
var points = [];
scp.map(function (s) {
s.MNNs.map(function (m, idx) {
if (!s.dovetailed[idx]) {
points.push([s.ontime, m, s.MPNs[idx], s.durations[idx], s.context.orig_points[idx][idxChan], s.context.orig_points[idx][idxVel]]);
}
});
});
return points.sort(mu.lex_more);
},
dovetail_durations: function dovetail_durations(stateContextPairs, param) {
var stateType = param.stateType;
var idxOn = param.indices.ontime;
var idxMNN = param.indices.MNN;
var idxMPN = param.indices.MPN;
var idxDur = param.indices.duration;
var idxChan = param.indices.channel;
var idxVel = param.indices.velocity;
// Get a last offtime.
// This is the ontime at which the final selected state began in the original
// piece.
var ontimeOfLastState = mu.max_argmax(stateContextPairs[stateContextPairs.length - 1].context.orig_points.map(function (p) {
return p[idxOn];
}))[0];
// This is the maximum offtime of a note in that state.
var offtimeOfLastState = mu.max_argmax(stateContextPairs[stateContextPairs.length - 1].context.orig_points.map(function (p) {
return p[idxOn] + p[idxDur];
}))[0];
// The difference between these two,
// offtimeOfLastState - ontimeOfLastState,
// will give us an acceptable value for an offtime for the final selected
// state in the new context, when added to the ontime for the final selected
// state.
var lastOfftime = stateContextPairs[stateContextPairs.length - 1].ontime + offtimeOfLastState - ontimeOfLastState;
// console.log("stateContextPairs[stateContextPairs.length - 1].ontime:", stateContextPairs[stateContextPairs.length - 1].ontime)
// console.log("ontimeOfLastState:", ontimeOfLastState)
// console.log("offtimeOfLastState:", offtimeOfLastState)
// console.log("lastOfftime:", lastOfftime)
// Dovetailing
// This involves going through each original point and seeing whether it
// lasted longer than the state to which it belongs. There are three cases to
// consider:
// (A) It does not. We assign a duration based on how long it does last in the
// state and we're done;
// (B) It does but the same pitch does not appear in the following selected
// state. We assign a duration up until where the next selected state begins
// and we're done;
// (C) It does and the same pitch appears in the following selected state. We
// carry on the search with this note in the following selected state now, and
// ask the same questions above (while-loop), until we find (A) or (B) be
// true. This is the most complex scenario, is tracked using an array called
// dovetail, and can lead to a pitch being tied across multiple selected
// states when being turned into a note.
// Set up durations and dovetailed.
stateContextPairs.map(function (s) {
s.durations = new Array(s.context.orig_points.length);
s.dovetailed = new Array(s.context.orig_points.length);
s.context.orig_points.map(function (p, idx) {
s.durations[idx] = 0;
s.dovetailed[idx] = false;
});
});
stateContextPairs.map(function (s, idx) {
// console.log("s['beat_MNN_state']:", s['beat_MNN_state'])
// Ontime where state began in original context.
var ontimeOfState = mu.max_argmax(s.context.orig_points.map(function (p) {
return p[idxOn];
}))[0];
// console.log("ontimeOfState:", ontimeOfState)
// Durations left in state
// console.log("orig_points:", s.context.orig_points)
var dlis = s.context.orig_points.map(function (p) {
return p[idxOn] + p[idxDur] - ontimeOfState;
});
// console.log("dlis:", dlis)
// These are the durations we will assign to each note (is parallel with
// the MNNs and MPNs properties, which should be present).
s.context.orig_points.map(function (p, kdx) {
// Stands for map into index.
var mii = s.context.map_into_state[kdx];
// const m = p[idxMNN]
// console.log("m:", m)
// Have a look in the next state.
var jdx = idx + 1;
while (jdx <= stateContextPairs.length) {
var compareOntime = void 0,
lins = void 0;
// Need to be careful about end case, where jdx == stateContextPairs.length
if (jdx < stateContextPairs.length) {
// Where this next state begins in new context.
// console.log("Regular case. jdx = " + jdx + ", stateContextPairs[jdx]:", stateContextPairs[jdx])
compareOntime = stateContextPairs[jdx].ontime;
// Stands for "look in next state". Is this "pitch" present?
//***********************************************
// 26.02.2020. FIXED FOR DIFFERENT STATE TYPES! *
//***********************************************
lins = stateContextPairs[jdx][stateType][1].indexOf(s[stateType][1][mii]);
} else {
// End case.
// console.log("End case")
compareOntime = lastOfftime; // Use value calculated above.
lins = -1; // Nothing to look for.
}
// console.log("compareOntime:", compareOntime, "lins:", lins)
if (dlis[kdx] <= compareOntime - stateContextPairs[jdx - 1].ontime) {
// Case (A)
// console.log("Case (A)")
s.durations[kdx] += dlis[kdx];
jdx = stateContextPairs.length;
} else if (lins == -1) {
// Case (B)
// console.log("Case (B)")
s.durations[kdx] += compareOntime - stateContextPairs[jdx - 1].ontime;
jdx = stateContextPairs.length;
} else {
// Case (C)
// console.log("Case (C)")
s.durations[kdx] += compareOntime - stateContextPairs[jdx - 1].ontime;
stateContextPairs[jdx].dovetailed[lins] = true;
// s.dovetailed[kdx] = true
}
jdx++;
}
});
// console.log("s.durations:", s.durations, "s.dovetailed:", s.dovetailed)
});
return stateContextPairs;
},
state_ontimes: function state_ontimes(stateContextPairs) {
var stateType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "beat_rel_sq_MNN_state";
var crotchetBeatsInBar = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 4;
var self = this;
var interStateDurations = stateContextPairs.map(function (s, idx) {
if (idx > 0) {
var d = s[stateType][0] - stateContextPairs[idx - 1][stateType][0];
if (d < 0) {
d = mu.mod(d, crotchetBeatsInBar);
} else if (d == 0) {
d = crotchetBeatsInBar;
}
return d;
}
});
// console.log("interStateDurations:", interStateDurations)
interStateDurations = interStateDurations.slice(1);
var ontimes = new Array(stateContextPairs.length);
ontimes[0] = stateContextPairs[0][stateType][0] - 1;
interStateDurations.map(function (isd, idx) {
ontimes[idx + 1] = ontimes[idx] + isd;
});
// console.log("ontimes:", ontimes)
return ontimes;
},
state_representation_of_pitch: function state_representation_of_pitch(midiMorphPair, param, tpc) {
var pointReconstruction = param.pointReconstruction;
var squashRange = param.squashRangeMidiMorph;
var mnn = midiMorphPair[0],
mpn = midiMorphPair[1];
switch (pointReconstruction) {
case "rel_sq_MNN":
// Remove tonic pitch closest.
mnn -= tpc[0];
mpn -= tpc[1];
// Squash.
while (mnn > squashRange[0] || mnn < -squashRange[0]) {
if (mnn > squashRange[0]) {
mnn -= squashRange[0];
mpn -= squashRange[1];
} else {
mnn += squashRange[0];
mpn += squashRange[1];
}
}
break;
case "rel_MNN":
// Remove tonic pitch closest.
mnn -= tpc[0];
mpn -= tpc[1];
break;
case "MNN":
// No manipulation required.
break;
default:
console.log("SHOULD NOT GET HERE!");
}
return [mnn, mpn];
},
get_suggestion: function get_suggestion(param) {
var stateType = param.stateType;
var stm = param.stm;
var initial = param.initial;
var nosConsecutives = param.nosConsecutives;
var ontimeUpperLimit = param.ontimeUpperLimit;
var randCount = param.randCount;
var idxOn = param.indices.ontime;
// const defaultTimeSig = { "topNo": 4, "bottomNo": 4 }
// console.log('stm[0][' + stateType + ']:', stm[0][stateType]);
// console.log('stm[5][' + stateType + ']:', stm[5][stateType]);
// Either take an initial provided state, choose one from the provided initial
// distribution, or choose one from beat 1 of the stm.
if (initial !== null) {
// It's an initial provided state or an initial distribution.
if (initial[stateType] !== undefined) {
// It's an initial provided state.
var lkState = initial;
} else {
// It's an initial distribution.
var lkState = mu.choose_one(initial);
randCount++;
}
} else {
// Choose an initial state from beat 1 of the stm.
var lkState = mu.choose_one(stm.filter(function (sc) {
return sc[stateType][0] == 1;
})); //[stateType]
randCount++;
}
// console.log("lkState:", lkState)
var lastOntime = lkState[stateType][0] - 1;
// console.log("lastOntime:", lastOntime)
// var lk_beat = lkState[0]
// var lk_mnns = lkState[1]
// console.log('lk_beat:', lk_beat);
// console.log('lk_mnns:', lk_mnns);
// Not using state string right now, but here's an example.
// var lkState_str = '1.5|48,60,67'; // Just an example.
// var lk_beat = parseFloat(lkState_str.split('|')[0]);
// var lkState = [lk_beat, lk_mnns];
// var lk_mnns = lkState_str.split('|')[1].split(',').map(function(m){ return parseFloat(m) });
// Use lkState and subsequent continuations to query the stm 40 times.
var stateCtxPairs = [lkState],
points = void 0;
lkState = lkState[stateType];
// console.log("stateCtxPairs:", stateCtxPairs)
// var nSt = 40; // This is the number of continuations.
// for (iSt = 0; iSt < nSt; iSt++){
while (lastOntime <= ontimeUpperLimit) {
var relIdx = mu.array_object_index_of_array(stm, lkState, stateType);
// console.log('relIdx:', relIdx);
if (relIdx == -1) {
console.log("Early stop: state was not found in the stm.");
break;
// return
// Choose a state at random.
// relIdx = mu.get_random_int(0, stm.length);
// console.log('rand populated relIdx:', relIdx);
}
// Use it to grab continuations and pick one at random.
var conts = stm[relIdx].continuations;
// console.log('stm[relIdx][stateType]:', stm[relIdx][stateType], 'conts.length:', conts.length);
var currCont = mu.choose_one(conts);
randCount++;
stateCtxPairs.push(currCont);
points = this.get_points_from_states(stateCtxPairs, param);
lastOntime = points[points.length - 1][idxOn];
// Update lkState.
lkState = currCont[stateType];
// console.log('new lkState:', lkState);
}
// Rest filtering done at analysis stage, but just checking.
// console.log("stateCtxPairs pre-rest filter:", stateCtxPairs.length)
// stateCtxPairs = stateCtxPairs.filter(function(sc){
// return sc.beat_MNN_state[1].length > 0;
// })
// console.log("stateCtxPairs post-rest filter:", stateCtxPairs.length)
// console.log("stateCtxPairs.slice(0, 5):", stateCtxPairs.slice(0, 5))
// stateCtxPairs.map(function(scPair){
// console.log("scPair.context.piece_id:", scPair.context.piece_id)
// })
return {
"randCount": randCount,
"stateContextPairs": stateCtxPairs,
"points": points
// // Converts [ beat, [MNNs]] format to 'beat|MNN1,MNN2,...,MNNn' format.
// stateCtxPairs = stateCtxPairs.map(function(sc){
// var state_str = sc.beat_MNN_state[0].toString() + "|";
// for (imnn = 0; imnn < sc.beat_MNN_state[1].length; imnn++){
// state_str = state_str + sc.beat_MNN_state[1][imnn].toString() + ',';
// }
// if (imnn > 0){
// state_str = state_str.slice(0, state_str.length - 1);
// }
// return {
// beatMNNState: state_str,
// orig_points: sc.context.orig_points.map(function(p){
// return {
// ontime: p[0],
// MNN: p[1],
// MPN: p[2],
// duration: p[3],
// staffNo: 3, // WARNING THIS IS SUPER HACKY!!
// velocity: p[5]
// }
// }),
// pieceId: sc.context.piece_id
// };
// })
// // console.log('stateCtxPairs:', stateCtxPairs);
// var suggested_notes = getNotesFromStates(
// stateCtxPairs, comp.notes, comp.timeSignatures, 0
// );
// var segs = segment(comp_obj2note_point_set({ notes: suggested_notes }));
// // We need all notes with ontimes greater than or equal to
// // segs[0].ontime, and less than or equal to
// // segs[0].ontime + 8 (assuming 4-4 time).
// suggested_notes = suggested_notes.filter(function(n){
// return n.ontime >= segs[0].ontime && n.ontime <= segs[0].ontime + 8;
// });
// // console.log('suggested_notes:', suggested_notes);
// comp.addNotes(suggested_notes);
// // console.log("comp.notes:", comp.notes);
// return comp;
};
},
get_lyrics_suggestion: function get_lyrics_suggestion(param) {
var stateType = param.stateType;
var stm = param.stm;
var initial = param.initial;
var nosConsecutives = param.nosConsecutives;
var wordLimit = param.wordLimit;
var randCount = param.randCount;
// const defaultTimeSig = { "topNo": 4, "bottomNo": 4 }
console.log('stm[0][' + stateType + ']:', stm[0][stateType]);
console.log('stm[5][' + stateType + ']:', stm[5][stateType]);
// Either take an initial provided state, choose one from the provided initial
// distribution, or choose one from beat 1 of the stm.
if (initial !== null) {
// It's an initial provided state or an initial distribution.
if (initial[stateType] !== undefined) {
// It's an initial provided state.
var lkState = initial;
} else {
// It's an initial distribution.
var lkState = mu.choose_one(initial);
randCount++;
}
} else {
// Choose an initial state from beat 1 of the stm.
var lkState = mu.choose_one(stm.filter(function (sc) {
return sc.context.index_in_line == 0;
})); //[stateType]
randCount++;
}
console.log("lkState:", lkState);
var nosWords = 1;
// Use lkState and subsequent continuations to query the stm.
var stateCtxPairs = [lkState],
words = void 0;
lkState = lkState[stateType];
console.log("stateCtxPairs:", stateCtxPairs);
while (nosWords <= wordLimit) {
var relIdx = mu.array_object_index_of_array(stm, lkState, stateType);
console.log('relIdx:', relIdx);
if (relIdx == -1) {
console.log("Early stop: state was not found in the stm.");
break;
}
// Use it to grab continuations and pick one at random.
var conts = stm[relIdx].continuations;
console.log('stm[relIdx][stateType]:', stm[relIdx][stateType], 'conts.length:', conts.length);
var currCont = mu.choose_one(conts);
randCount++;
stateCtxPairs.push(currCont);
words = get_lyrics_from_states(stateCtxPairs, param);
nosWords++;
// Update lkState.
lkState = currCont[stateType];
console.log('new lkState:', lkState);
}
return {
"randCount": randCount,
"stateContextPairs": stateCtxPairs,
"words": words
};
}
};