UNPKG

maia-markov

Version:

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

603 lines (517 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_duple.js", "initial": __dirname + "/stm/strq2020_duple_initial.js", "final": __dirname + "/stm/strq2020_duple_final.js", "outputDir": __dirname + "/out/strq2020_duple/" }, "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 = [ [ "Almond", "Birch", "Chestnut", "Cress", "Daisy", "Elderberry", "Fennel", "Flax", "Garlic", "Holly", "Ivy", "Juniper", "Kudzu", "Lilac", "Maple", "Moosewood", "Nightshade", "Olive", "Pear", "Quercitron", "Rose", "Snowdrop", "Sunflower", "Tea", "Thistle", "Violet", "Walnut", "Willow", "Yarrow", "Zebrawood" ], [ "Alligator", "Armadillo", "Bee", "Butterfly", "Cat", "Chameleon", "Dove", "Dragonfly", "Elephant", "Firefly", "Gopher", "Hippopotamus", "Hummingbird", "Iguana", "Jay", "Leopard", "Lion", "Lynx", "Magpie", "Mosquito", "Nightingale", "Orangutan", "Orca", "Peacock", "Slug", "Tiger", "Toad", "Tortoise", "Whale", "Zebra" ] ] let param = { "stateType": "beat_rel_sq_MNN_state", "pointReconstruction": "rel_sq_MNN", "timeSignatures": [ {"barNo": 1, "topNo": 4, "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": 16 }, { "label": "A2", "ontimeBgn": 16, "ontimeEnd": 32 }, { "label": null, "ontimeBgn": 32, "ontimeEnd": 60 } ], // 2. Let's say we have ABABCA. [ { "label": "A1", "ontimeBgn": 0, "ontimeEnd": 8 }, { "label": "B1", "ontimeBgn": 8, "ontimeEnd": 16 }, { "label": "A2", "ontimeBgn": 16, "ontimeEnd": 24 }, { "label": "B2", "ontimeBgn": 24, "ontimeEnd": 32 }, { "label": null, "ontimeBgn": 32, "ontimeEnd": 40 }, { "label": "A3", "ontimeBgn": 40, "ontimeEnd": 48 }, { "label": null, "ontimeBgn": 48, "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 if (nPairs > 0){ param.initial = win[1].scp[nPairs - 1] } else { param.initial = JSON.parse(initialStr) } 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) // ) // })