UNPKG

maia-markov

Version:

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

516 lines (485 loc) 19 kB
// Imports const mu = require('maia-util') // import 'maia-util' // import mu from 'maia-util' // Constructor for Generator object export default 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(a, n){ return a - (n*Math.floor(a/n)) }, get_lyrics_from_states: function(stateContextPairs, param){ const stateType = param.stateType // Make a fresh copy because I was getting some weird-ass problems with idx. let scp = JSON.parse(JSON.stringify(stateContextPairs)) // console.log("scp:", scp) // Unpack states into a string. console.log("scp[0]:", scp[0]) let 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(stateContextPairs, param){ const self = this const stateType = param.stateType const pointReconstruction = param.pointReconstruction const currTimeSig = param.timeSignatures[0] const crotchetBeatsInBar = 4*currTimeSig.topNo/currTimeSig.bottomNo const idxOn = param.indices.ontime const idxMNN = param.indices.MNN const idxMPN = param.indices.MPN const idxDur = param.indices.duration const idxChan = param.indices.channel const 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. let scp = JSON.parse(JSON.stringify(stateContextPairs)) // console.log("scp:", scp) // Unpack states into MNNs and MPNs. scp.forEach(function(s){ let MNNs = [] let MPNs = [] s.context.orig_points.forEach(function(p){ const 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. let 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. let 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(stateContextPairs, param){ const stateType = param.stateType const idxOn = param.indices.ontime const idxMNN = param.indices.MNN const idxMPN = param.indices.MPN const idxDur = param.indices.duration const idxChan = param.indices.channel const idxVel = param.indices.velocity // Get a last offtime. // This is the ontime at which the final selected state began in the original // piece. const 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. const 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. const 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. const 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) const 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. const mii = s.context.map_into_state[kdx] // const m = p[idxMNN] // console.log("m:", m) // Have a look in the next state. let jdx = idx + 1 while (jdx <= stateContextPairs.length){ let compareOntime, lins // 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( stateContextPairs, stateType = "beat_rel_sq_MNN_state", crotchetBeatsInBar = 4 ){ const self = this let interStateDurations = stateContextPairs.map(function(s, idx){ if (idx > 0){ let 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) const 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(midiMorphPair, param, tpc){ const pointReconstruction = param.pointReconstruction const squashRange = param.squashRangeMidiMorph let 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(param){ const stateType = param.stateType const stm = param.stm const initial = param.initial const nosConsecutives = param.nosConsecutives const ontimeUpperLimit = param.ontimeUpperLimit let randCount = param.randCount const 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) let 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. let stateCtxPairs = [lkState], points 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(param){ const stateType = param.stateType const stm = param.stm const initial = param.initial const nosConsecutives = param.nosConsecutives const wordLimit = param.wordLimit let 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) let nosWords = 1 // Use lkState and subsequent continuations to query the stm. let stateCtxPairs = [lkState], words 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 } } }