maia-markov
Version:
Markov analysis and generation functions supporting various applications by Music Artificial Intelligence Algorithms, Inc.
408 lines (379 loc) • 10.3 kB
JavaScript
// Copyright Tom Collins, 24.9.2020
// Generating material for https://boblsturm.github.io/aimusic2020/.
// Requires
const path = require("path")
const fs = require("fs")
const sr = require('seed-random')
const { Midi } = require('@tonejs/midi')
const mu = require('maia-util')
const mm = require("./../dist/index")
// Individual user paths
const mainPaths = {
"tom": {
"stm": path.join(__dirname, "stm", "aimgc2020_6_8.js"),
"initial": path.join(__dirname, "stm", "aimgc2020_6_8_initial.js"),
"final": path.join(__dirname, "stm", "aimgc2020_6_8_final.js"),
"outputDir": path.join(__dirname, "out", "aimgc2020_6_8"),
"outputBin": path.join(__dirname, "out", "aimgc2020_6_8_bin")
},
"anotherUser": {
"stm": "",
"initial": "",
"outputDir": ""
}
}
// Set up parameters.
let param = {
"stateType": "beat_rel_MNN_state",
// "stateType": "beat_rel_sq_MNN_state",
"pointReconstruction": "rel_MNN",
// "pointReconstruction": "rel_sq_MNN",
"timeSignatures": [ {"barNo": 1, "topNo": 6, "bottomNo": 8, "ontime": 0} ],
"stm": null,
"graph": null,
"initial": null,
"final": null,
"nosConsecutives": 4,
"ontimeUpperLimit": null,
"beatHistGranularity": 4,
// "squashRangeMidiMorph": [12, 7],
"indices": {
"ontime": 0, "MNN": 1, "MPN": 2, "duration": 3, "channel": 4, "velocity": 5
},
"randCount": 0,
"nosGenerate": 17500,
"nosOutput": 8750,
"nosOutputBin": 5
}
let seeds = []
for (let i = 0; i < param.nosGenerate; i++){
seeds.push(i.toString())
}
// console.log("seeds:", seeds)
let repetitiveStructures = [
// 1. Repetitive structure of "Money in both pockets".
// D1--- D2---
// B1 B2 C1 C2 B3 C3
// A1 A2
[
{// 0
"label": "A1",
"ontimeBgn": 0,
"ontimeEnd": 6,
"subsetScore": 1
},
{// 1
"label": "null",
"ontimeBgn": 6,
"ontimeEnd": 12,
"subsetScore": null
},
{// 2
"label": "A2",
"ontimeBgn": 12,
"ontimeEnd": 18,
"subsetScore": 1
},
{// 3
"label": "null",
"ontimeBgn": 18,
"ontimeEnd": 24,
"subsetScore": null
},
{// 4
"label": "A3",
"ontimeBgn": 24,
"ontimeEnd": 30,
"subsetScore": 1
},
{// 5
"label": "null",
"ontimeBgn": 30,
"ontimeEnd": 36,
"subsetScore": null
},
{// 6
"label": "A4",
"ontimeBgn": 36,
"ontimeEnd": 42,
"subsetScore": 1
},
{// 7
"label": "null",
"ontimeBgn": 42,
"ontimeEnd": 48,
"subsetScore": null
},
{// 8
"label": "C1",
"ontimeBgn": 48,
"ontimeEnd": 72,
"subsetScore": 0
},
{// 9
"label": "C2",
"ontimeBgn": 72,
"ontimeEnd": 96,
"subsetScore": 0
},
{ // 10
"label": "A5",
"ontimeBgn": 96,
"ontimeEnd": 102,
"subsetScore": 1
},
{// 11
"label": "null",
"ontimeBgn": 102,
"ontimeEnd": 108,
"subsetScore": null
},
{// 12
"label": "A6",
"ontimeBgn": 108,
"ontimeEnd": 114,
"subsetScore": 1
},
{// 13
"label": "null",
"ontimeBgn": 114,
"ontimeEnd": 120,
"subsetScore": null
},
{// 14
"label": "C3",
"ontimeBgn": 120,
"ontimeEnd": 144,
"subsetScore": 0
},
],
// 2. Repetitive structure of ?
[
]
]
// 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.map(function(seed, idx){
if (idx % 500 === 0){
console.log("Generating " + (idx + 1) + " of " + seeds.length + ".")
}
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.
// Handling occurrences of A.
param.ontimeUpperLimit = win[0].ontimeEnd
win[0].gen = gn.get_suggestion(param)
win[0].scp = win[0].gen.stateContextPairs
// Copy to wins 2, 4, 6, 10, and 12.
let arr = [2, 4, 6, 10, 12]
arr.forEach(function(idx){
win[idx].scp = JSON.parse(JSON.stringify(win[0].scp))
})
// Handling occurrences of B.
// B1 contains A1 and A2, so fill in gaps which are at 6-12 and 18-24, then
// paste to corresponding locations in B2 and B3.
// 6-12
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)
// 18-24
param.ontimeUpperLimit = win[3].ontimeEnd - win[3].ontimeBgn
nPairs = win[2].scp.length
if (nPairs > 0){
param.initial = win[2].scp[nPairs - 1]
}
else {
param.initial = JSON.parse(initialStr)
}
win[3].gen = gn.get_suggestion(param)
win[3].scp = win[3].gen.stateContextPairs.slice(1)
// Paste.
// Copy to win[1] to wins 5 and 11.
arr = [5, 11]
arr.forEach(function(idx){
win[idx].scp = JSON.parse(JSON.stringify(win[1].scp))
})
// Copy to win[3] to wins 7 and 13.
arr = [7, 13]
arr.forEach(function(idx){
win[idx].scp = JSON.parse(JSON.stringify(win[3].scp))
})
// Handling occurrences of C.
param.ontimeUpperLimit = win[8].ontimeEnd - win[8].ontimeBgn
nPairs = win[7].scp.length
if (nPairs > 0){
param.initial = win[7].scp[nPairs - 1]
}
else {
param.initial = JSON.parse(initialStr)
}
win[8].gen = gn.get_suggestion(param)
win[8].scp = win[8].gen.stateContextPairs.slice(1)
// Copy to win[8] to wins 9 and 14.
arr = [9, 14]
arr.forEach(function(idx){
win[idx].scp = JSON.parse(JSON.stringify(win[8].scp))
})
// Concatenate scp properties.
let scp = []
win.forEach(function(w, idx){
if (w.scp !== undefined){
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.forEach(function(p){
// p[param.indices.ontime] += w.ontimeBgn
p[param.indices.MNN] += mnnShift
p[param.indices.MPN] += mnnShift
})
// console.log("points:", points)
// Calculate metrics.
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,
"points": points,
"proportionOfRests": prop,
"tonalAmbiguity": ta,
"beatHistEntropy": en,
"empiricalHeuristic": prop*ta*en
}
})
// Sort by empirically-inspired heuristics.
seedMetrics = seedMetrics.sort(function(a, b){
return a.empiricalHeuristic - b.empiricalHeuristic
})
console.log("seedMetrics.slice(0, 3):", seedMetrics.slice(0, 3))
console.log("seedMetrics.slice(seeds.length - 3):", seedMetrics.slice(seeds.length - 3))
function points2midi(pts, thePath, sf = 1){
let ontimeCorrection = 0
const minOntime = mu.min_argmin(pts.map(function(p){ return p[param.indices.ontime] }))[0]
if (minOntime < 0){
ontimeCorrection = 4*param.timeSignatures[0].topNo/param.timeSignatures[0].bottomNo
}
let midi = new Midi()
// "Works" but actually changes nothing!:
// midi.header.setTempo(240)
// console.log("midi.header:", midi.header)
let track = midi.addTrack()
pts.forEach(function(p){
track.addNote({
midi: p[param.indices.MNN],
time: sf*(p[param.indices.ontime] + ontimeCorrection),
duration: sf*p[param.indices.duration],
velocity: p[param.indices.velocity]
})
})
fs.writeFileSync(
thePath,
new Buffer(midi.toArray())
)
}
// Convert "best-performing" excerpts to MIDI.
const scaleFactor = 0.5
for (let i = 0; i < param.nosOutput; i++){
points2midi(
seedMetrics[i].points,
path.join(pathsEtc.outputDir, (i + 1) + ".mid"),
scaleFactor
)
}
// Convert "worst-performing" excerpts to MIDI.
for (let i = param.nosGenerate - param.nosOutputBin; i < param.nosGenerate; i++){
points2midi(
seedMetrics[i].points,
path.join(pathsEtc.outputBin, (i + 1) + ".mid"),
scaleFactor
)
}