UNPKG

maia-markov

Version:

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

598 lines (512 loc) 17.3 kB
// Copyright Tom Collins, 26.5.2020 // Having a look at loading on to a Graph class and using shortest path. // Individual user paths. const mainPaths = { "tom": { "stm": __dirname + "/stm/strq2020_triple.js", "initial": __dirname + "/stm/strq2020_triple_initial.js", "final": __dirname + "/stm/strq2020_triple_final.js", "outputDir": __dirname + "/out/strq2020_triple/" }, "anotherUser": { "stm": "", "initial": "", "outputDir": "" } } // Requires. const fs = require("fs") const sr = require('seed-random') const { Midi } = require('@tonejs/midi') // const gn = require("./generate") // const an = require("./analyze") const mu = require('maia-util') const mm = require("./../dist/index") // Set up parameters. let seeds = [ [ "BBQ chicken", "Bianca", "Calzone", "Caprese", "Capricciosa", "Dessert", "Diavola", "Focaccia", "Funghi", "Frutti di mare", "Hawaiian", "La Reine", "Liguria", "Margherita", "Marinara", "Napoletana", "Pepperoni", "Pugliese", "Prosciutto", "Quattro formagi", "Quattro stagioni", "Romana", "Sfincione", "Sicilian", "Sloppy Guiseppe", "Tirolese", "Tuna", "Vegetariana", "Veronese", "Viennese" ], [ "Hydrogen", "Helium", "Lithium", "Beryllium", "Boron", "Carbon", "Nitrogen", "Oxygen", "Fluorine", "Neon", "Sodium", "Magnesium", "Aluminum", "Silicon", "Phosphorus", "Sulfur", "Chlorine", "Argon", "Potassium", "Calcium", "Scandium", "Titanium", "Vanadium", "Chromium", "Manganese", "Iron", "Cobalt", "Nickel", "Copper", "Zinc" ] ] let param = { "stateType": "beat_rel_sq_MNN_state", "pointReconstruction": "rel_sq_MNN", "timeSignatures": [ {"barNo": 1, "topNo": 3, "bottomNo": 4, "ontime": 0} ], "stm": null, "graph": null, "initial": null, "final": null, "nosConsecutives": 4, "nosCandidates": 1, "ontimeUpperLimit": null, "beatHistGranularity": 4, "squashRangeMidiMorph": [12, 7], "indices": { "ontime": 0, "MNN": 1, "MPN": 2, "duration": 3, "channel": 4, "velocity": 5 }, "randCount": 0 } let repetitiveStructures = [ // 1. Let's say the first 4 bars are repeated in bars 5-9, and then the excerpt // proceeds without further repetitions. [ { "label": "A1", "ontimeBgn": 0, "ontimeEnd": 12 }, { "label": "A2", "ontimeBgn": 12, "ontimeEnd": 24 }, { "label": null, "ontimeBgn": 24, "ontimeEnd": 60 } ], // 2. Let's say we have ABABCA. [ { "label": "A1", "ontimeBgn": 0, "ontimeEnd": 12 }, { "label": "B1", "ontimeBgn": 12, "ontimeEnd": 18 }, { "label": "A2", "ontimeBgn": 18, "ontimeEnd": 30 }, { "label": "B2", "ontimeBgn": 30, "ontimeEnd": 36 }, { "label": null, "ontimeBgn": 36, "ontimeEnd": 42 }, { "label": "A3", "ontimeBgn": 42, "ontimeEnd": 54 }, { "label": null, "ontimeBgn": 54, "ontimeEnd": 60 }, ] ] // Grab user name from command line to set path to data. let nextU = false let pathsEtc; process.argv.forEach(function(arg, ind){ if (arg === "-u"){ nextU = true } else if (nextU){ pathsEtc = mainPaths[arg] nextU = false } }) // fs.mkdir(outdir); const an = new mm.Analyzer() const gn = new mm.Generator() const pg = new mm.PatternGenerator() const stmStr = fs.readFileSync(pathsEtc.stm) param.stm = JSON.parse(stmStr) const initialStr = fs.readFileSync(pathsEtc.initial) param.initial = JSON.parse(initialStr) const finalStr = fs.readFileSync(pathsEtc.final) param.final = JSON.parse(finalStr) // Helper functions for rating output. function beat_histogram( aPointSet, beatsInMeasure = 4, granularity = 4, showTF = false ){ var hist = [] for (var i = 0; i < beatsInMeasure*granularity; i++){ hist[i] = 0 } aPointSet.forEach(function(p, idx){ var intPart = parseInt(p[0]) var decPart = p[0] - intPart var beat = intPart % beatsInMeasure + decPart var histIdx = Math.floor(granularity*beat) hist[histIdx]++ }) if (showTF){ console.log('hist:', hist) } return hist } function normalise_array(anArray){ const s = mu.array_sum(anArray) return anArray.map(function(a){ return a/s }) } function entropy(aDist){ return mu.array_sum(aDist.map(function(p){ if (p == 0){ return 0 } return -p*Math.log2(p) })) } function duration_of_rests(aPointSet, ontimeUpperLimit){ const segs = mu.segment(aPointSet) let dur = 0 segs.forEach(function(seg){ if (seg.points.length == 0){ dur += seg.offtime - seg.ontime } }) if (segs[segs.length - 1].offtime < ontimeUpperLimit){ dur += ontimeUpperLimit - segs[segs.length - 1].offtime } return dur } function tonal_ambiguity(aPointSet){ const fsm = mu.fifth_steps_mode( aPointSet, mu.krumhansl_and_kessler_key_profiles, 1, 3 ) return 1 - Math.abs(fsm[1]) } let seedMetrics = seeds[0].map(function(seed){ sr(seed, {global: true}) // Refresh win definition. let win = JSON.parse(JSON.stringify(repetitiveStructures[0])) // Still to implement Lisp code for pattern inheritance in JS. param.ontimeUpperLimit = win[0].ontimeEnd win[0].gen = gn.get_suggestion(param) win[0].scp = win[0].gen.stateContextPairs // Copy to win[1]. win[1].scp = JSON.parse(JSON.stringify(win[0].scp)) param.ontimeUpperLimit = win[2].ontimeEnd - win[2].ontimeBgn let nPairs = win[1].scp.length param.initial = win[1].scp[nPairs - 1] win[2].gen = gn.get_suggestion(param) win[2].scp = win[2].gen.stateContextPairs.slice(1) // console.log("win:", win) // Concatenate scp properties. let scp = [] win.forEach(function(w){ scp = scp.concat(w.scp) }) // console.log("scp:", scp) // Reinstate full initial distribution. param.initial = JSON.parse(initialStr) let points = gn.get_points_from_states(scp, param) const mnnShift = win[0].scp[0].context.tonic_pitch_closest[0] const mpnShift = win[0].scp[0].context.tonic_pitch_closest[1] points.map(function(p){ // p[param.indices.ontime] += w.ontimeBgn p[param.indices.MNN] += mnnShift p[param.indices.MPN] += mnnShift }) // console.log("points:", points) // let pg = new mm.PatternGenerator() // const stuff = pg.generate(30, param) // console.log("stuff.psMetrics[25].pointSet:", stuff.psMetrics[25].pointSet) // // const points = stuff.psMetrics[25].pointSet.map(function(p){ // p[param.indices.MNN] += stuff.psMetrics[25].stateCtxPairs[0].context // .tonic_pitch_closest[0] //60 // p[param.indices.MPN] += stuff.psMetrics[25].stateCtxPairs[0].context // .tonic_pitch_closest[1] //60 // return p // }) // console.log("points:", points) // Save points as a MIDI file and state-context pairs as a text file. const sf = 0.5 let midi = new Midi() // "Works" but actually changes nothing!: // midi.header.setTempo(240) // console.log("midi.header:", midi.header) let track = midi.addTrack() points.forEach(function(p){ track.addNote({ midi: p[param.indices.MNN], time: sf*p[param.indices.ontime], duration: sf*p[param.indices.duration], velocity: p[param.indices.velocity] }) }) fs.writeFileSync( pathsEtc.outputDir + seed + ".mid", new Buffer(midi.toArray()) ) const prop = duration_of_rests(points, win[win.length - 1].ontimeEnd)/ win[win.length - 1].ontimeEnd const ta = tonal_ambiguity(points) const bh = beat_histogram(points, param.timeSignatures[0].topNo, param.beatHistGranularity) const en = entropy(normalise_array(bh)) return { "seed": seed, "proportionOfRests": prop, "tonalAmbiguity": ta, "beatHistEntropy": en } }) console.log("seedMetrics:", seedMetrics.sort(function(a, b){ return a.proportionOfRests*a.tonalAmbiguity*a.beatHistEntropy - b.proportionOfRests*b.tonalAmbiguity*b.beatHistEntropy })) seedMetrics = seeds[1].map(function(seed){ sr(seed, {global: true}) // Refresh win definition. let win = JSON.parse(JSON.stringify(repetitiveStructures[1])) // Still to implement Lisp code for pattern inheritance in JS. // A. param.ontimeUpperLimit = win[0].ontimeEnd win[0].gen = gn.get_suggestion(param) win[0].scp = win[0].gen.stateContextPairs // Copy to win[2]. win[2].scp = JSON.parse(JSON.stringify(win[0].scp)) // Copy to win[5]. win[5].scp = JSON.parse(JSON.stringify(win[0].scp)) // B. param.ontimeUpperLimit = win[1].ontimeEnd - win[1].ontimeBgn let nPairs = win[0].scp.length if (nPairs > 0){ param.initial = win[0].scp[nPairs - 1] } else { param.initial = JSON.parse(initialStr) } win[1].gen = gn.get_suggestion(param) win[1].scp = win[1].gen.stateContextPairs.slice(1) // Copy to win[3]. win[3].scp = JSON.parse(JSON.stringify(win[1].scp)) // nulls. param.ontimeUpperLimit = win[4].ontimeEnd - win[4].ontimeBgn nPairs = win[3].scp.length if (nPairs > 0){ param.initial = win[3].scp[nPairs - 1] } else { param.initial = JSON.parse(initialStr) } win[4].gen = gn.get_suggestion(param) win[4].scp = win[4].gen.stateContextPairs.slice(1) param.ontimeUpperLimit = win[6].ontimeEnd - win[6].ontimeBgn nPairs = win[5].scp.length if (nPairs > 0){ param.initial = win[5].scp[nPairs - 1] } else { param.initial = JSON.parse(initialStr) } win[6].gen = gn.get_suggestion(param) win[6].scp = win[6].gen.stateContextPairs.slice(1) // Concatenate scp properties. let scp = [] win.forEach(function(w){ scp = scp.concat(w.scp) }) // console.log("scp:", scp) // Reinstate full initial distribution. param.initial = JSON.parse(initialStr) let points = gn.get_points_from_states(scp, param) const mnnShift = win[0].scp[0].context.tonic_pitch_closest[0] const mpnShift = win[0].scp[0].context.tonic_pitch_closest[1] points.map(function(p){ // p[param.indices.ontime] += w.ontimeBgn p[param.indices.MNN] += mnnShift p[param.indices.MPN] += mnnShift }) // console.log("points:", points) // Save points as a MIDI file and state-context pairs as a text file. const sf = 0.5 let midi = new Midi() let track = midi.addTrack() points.forEach(function(p){ track.addNote({ midi: p[param.indices.MNN], time: sf*p[param.indices.ontime], duration: sf*p[param.indices.duration], velocity: p[param.indices.velocity] }) }) fs.writeFileSync( pathsEtc.outputDir + seed + ".mid", new Buffer(midi.toArray()) ) const prop = duration_of_rests(points, win[win.length - 1].ontimeEnd)/ win[win.length - 1].ontimeEnd const ta = tonal_ambiguity(points) const bh = beat_histogram(points, param.timeSignatures[0].topNo, param.beatHistGranularity) const en = entropy(normalise_array(bh)) return { "seed": seed, "proportionOfRests": prop, "tonalAmbiguity": ta, "beatHistEntropy": en } }) console.log("seedMetrics:", seedMetrics.sort(function(a, b){ return a.proportionOfRests*a.tonalAmbiguity*a.beatHistEntropy - b.proportionOfRests*b.tonalAmbiguity*b.beatHistEntropy })) // console.log("param.stm[0].beat_rel_sq_MNN_state:", param.stm[0].beat_rel_sq_MNN_state) // console.log("param.stm[0].continuations:", param.stm[0].continuations) // const exampleState = param.stm[0].beat_rel_sq_MNN_state // console.log("an.state2string(exampleState):", an.state2string(exampleState)) // let readyForGraph = param.stm.map(function(scPair, idx){ // // if (idx % 100 == 0){ // // console.log("On state " + idx + " of " + param.stm.length + ".") // // } // const s = an.state2string(scPair.beat_rel_sq_MNN_state) // const cs = mu.get_unique( // scPair.continuations.map(function(c){ // return an.state2string(c.beat_rel_sq_MNN_state) // }) // .map(function(c){ // return { "state": c } // }) // ) // return { "state": s, "continuations": cs } // }) // // console.log("readyForGraph.slice(0, 10):", readyForGraph.slice(0, 10)) // param.graph = new mm.Graph(readyForGraph, "state", "continuations") // const seed = seeds[9] // sr(seed, {global: true}) // const initialScPair = mu.choose_one(param.initial) // const initialState = initialScPair[param.stateType] // const initialStateStr = an.state2string(initialState) // console.log("initialState:", initialState) // // const finalState = mu.choose_one(param.final).beat_rel_sq_MNN_state // const finalStateStr = an.state2string(finalState) // console.log("finalState:", finalState) // // const path = param.graph.print_shortest_path(initialStateStr, finalStateStr) // console.log("path:", path) // let sthg = an.string2state(path[1]) // console.log("sthg:", sthg) // // const preferredIdx = 0 // win[0].gen = pg.generate(param.nosCandidates, param) // win[0].scp = win[0].gen.psMetrics[preferredIdx].stateCtxPairs // // Copy to win[2]. // win[2].scp = JSON.parse(JSON.stringify(win[0].scp)) // // win[1].gen = pg.generate(param.nosCandidates, param) // win[1].scp = win[1].gen.psMetrics[preferredIdx].stateCtxPairs // // Copy to win[3]. // win[3].scp = JSON.parse(JSON.stringify(win[1].scp)) // // let nPairs = win[1].gen.psMetrics[preferredIdx].stateCtxPairs.length // param.initial = win[1].gen.psMetrics[preferredIdx].stateCtxPairs[nPairs - 1] // win[4].gen = gn.get_suggestion(param) // win[4].scp = win[4].gen.stateContextPairs.slice(1) // // console.log("win:", win) // // Concatenate scp properties. // let scp = [] // win.forEach(function(w){ // scp = scp.concat(w.scp) // }) // // console.log("scp:", scp) // Reconstruct state-context pairs. // let scPairs = [initialScPair] // for (let i = 1; i < path.length; i++){ // const stateProbe = scPairs[i - 1][param.stateType] // const relIdx = mu.array_object_index_of_array(param.stm, stateProbe, param.stateType) // // console.log('relIdx:', relIdx) // const contProbe = an.string2state(path[i]) // const relConts = param.stm[relIdx].continuations.filter(function(c){ // return c[param.stateType].equals(contProbe) // }) // // console.log('relConts:', relConts) // if (relConts.length == 0){ // console.log("ERROR! THERE SHOULD BE AT LEAST ONE!") // return // } // scPairs[i] = mu.choose_one(relConts) // } // console.log("scPairs:", scPairs) // // let points = gn.get_points_from_states(scPairs, param) // // // points = points.map(function(p){ // p[param.indices.MNN] += scPairs[0].context // .tonic_pitch_closest[0] //60 // p[param.indices.MPN] += scPairs[0].context // .tonic_pitch_closest[1] //60 // return p // }) // console.log("points:", points) // // // var relIdx = mu.array_object_index_of_array(param.stm, initialState, "beat_rel_sq_MNN_state") // console.log('relIdx:', relIdx); // console.log( // "param.stm[relIdx].continuations[0].beat_rel_sq_MNN_state:", // param.stm[relIdx].continuations[0].beat_rel_sq_MNN_state // ) // console.log("g.print_neighbors('1|-12,3'):", g.print_neighbors('1|-12,3')) // const someState = an.state2string(mu.choose_one(param.stm).beat_rel_sq_MNN_state) // console.log("someState:", someState) // const someState2 = an.state2string(mu.choose_one(param.stm).beat_rel_sq_MNN_state) // console.log("someState2:", someState2) // const path = g.print_shortest_path(initialStateStr, someState) // console.log("path:", path) // let sthg = an.string2state(path[1]) // console.log("sthg:", sthg) // Seed random number generation. // sr('christianeriksen', {global: true}); // Overrides global Math.random. // var numA = Math.random(); // console.log(numA); // sr.resetGlobal();// Reset to default Math.random. // seeds.map(function(seed){ // // Seed random number generation. // sr(seed, {global: true}) // // Reset randCount, then generate states and points. // param.randCount = 0 // console.log("randCount before get_abs_suggestion:", param.randCount) // var gendOutput = gn.get_suggestion(param) // console.log("randCount after get_abs_suggestion:", gendOutput.randCount) // // console.log("gendOutput.points:", gendOutput.points) // // // Convert points to a key according to the initial state. // gendOutput.points = gendOutput.points.map(function(p){ // p[param.indices.MNN] += gendOutput.stateContextPairs[0].context // .tonic_pitch_closest[0] //60 // p[param.indices.MPN] += gendOutput.stateContextPairs[0].context // .tonic_pitch_closest[1] //60 // return p // }) // // // Save points as a MIDI file and state-context pairs as a text file. // let midi = new Midi() // console.log("midi.header:", midi.header) // let track = midi.addTrack() // gendOutput.points.map(function(p){ // track.addNote({ // midi: p[param.indices.MNN], // time: p[param.indices.ontime], // duration: p[param.indices.duration], // velocity: p[param.indices.velocity] // }) // }) // fs.writeFileSync( // pathsEtc.outputDir + seed + ".mid", // new Buffer(midi.toArray()) // ) // // track.notes = track.notes.map(function(n){ // // n.time *= 0.5 // // n.duration *= 0.5 // // return n // // }) // // fs.writeFileSync( // // pathsEtc.outputDir + "chords_" + seed + "_ableton.mid", // // new Buffer(midi.toArray()) // // ) // fs.writeFileSync( // pathsEtc.outputDir + seed + ".txt", // JSON.stringify(gendOutput.stateContextPairs, null, 2) // ) // })