Randomizing music with MuseScore and Node.js

One of my esteemed professors from Santa Monica College, Dr. Driscoll asked for an opinion on how one can use a sheet of music and reshuffle some measures to generate a unique exercise for each student. This turned out more fun than anticipated and here’s a solution I came up with using the free notation […]


One of my esteemed professors from Santa Monica College, Dr. Driscoll asked for an opinion on how one can use a sheet of music and reshuffle some measures to generate a unique exercise for each student. This turned out more fun than anticipated and here's a solution I came up with using the free notation software MuseScore and a Node.js script. I hope it can be useful to someone else in the future for music education or why not for generative music too.

For the inpatient, here's the code.

3-step process

  1. Create a MuseScore file to be used as a "template"
  2. Reshuffle the measures by manipulating XML in a Node.js script and spit out an N number of different MuseScore files
  3. Use a batch-convert plugin in MuseScore to convert the new files to PDF, MP3, MIDI or any other desired output format

The template

For a template you can create a new file in MuseScore or import some other file into MuseScore for final touches. In my case, the prof gave me a MusicXML file exported from Sibelius (a clunkier commercial alternative to MuseScore).

Once the music sheet is to your liking, export it as uncompressed XML, native to MuseScore (*.mscx).

Turns out MuseScore's native files are compressed xml (mscz) or its uncompressed brethren (mscx). I used the uncompressed version so I can look around into the XML and also don't have to deal with compression in my Node script.

Why MuseScore's XML and not MusicXML? I don't have a good answer other than convenience and habit and reducing one more variable.

In the template you pick certain measures be reused and reshuffled, for example motive A consist of measures 2 and 3, motive C is just measure 8 and so on. These motives will be defined in the Node script.

The script

The script does this:

  1. Read the XML template using xml-js into a JavaScript object for manipulation
  2. Extract the motive measures from the XML
  3. Generate 100 random permutations of the desired motives and their quantity
  4. Write 100 new XML files with the recombined measures

But first...

Configuration

const MAX = 100; // how many combinations to generate
const ADJOK = false; // is it ok to have adjacent repeated motives
const motives = {
  A: [2, 3], // Motive A is measures 2 and 3
  B: [4, 5],
  C: [8],
  D: [10, 11],
  E: [16],
  F: [17],
  G: [19],
  H: [22],
  I: [23],
};
// we want motive A to happen twice in the new score,
// motive C to happen 4 times and so on
const distribution = 'AADDFFEEEBGHICCCC';
const OUT = 'out/';

The funny-looking AADDFFEEEBGHICCCC is a definition of how many times you want each motive to repeat. This is what's going to be reshuffled to create the new combinations.

Read the XML

// imports
const convert = require('xml-js');
const fs = require('fs');

const xml = fs.readFileSync('Template.mscx', 'utf8');
const options = {compact: true, ignoreComment: true, spaces: 4};
const source = convert.xml2js(xml, options);

convert is the XML-JS library that lets you convert to/from XML, JSON and JavaScript objects. Here we convert the XML to a JavaScript object for easy manipulation.

Next, remembering the location of the measures (a Measure array in the resulting object) for less typing:

// an array of all measures
const origMeasures = source.museScore.Score.Staff.Measure;

Then, going through the motives configuration and reading them from the template score:

// extract the patterns from the template
const patterns = {};
Object.keys(motives).forEach((letter) => {
  patterns[letter] = [];
  motives[letter].forEach((m) => {
    // measures start from 1, arrays from 0
    patterns[letter].push(origMeasures[m - 1]); 
  });
});

Generate 100 random permutations

The variable combinations will contain the new reshuffled strings (e.g. ACGFCDCEFIHEDEBCA, GIECBFCADCHAEFCED and so on).

Using a Set prevents duplicates.

// generate MAX random combinations
const combinations = new Set();
let these = distribution.split('');
while (combinations.size < MAX) {
  these.sort(() => 0.5 - Math.random());
  if (checkAdjecents(these)) {
    combinations.add(these.join(''));
  }
}

A helper function to disallow adjacent motives, if desired:

function checkAdjecents(combo) {
  if (ADJOK) {
    return true;
  }
  for (let i = 1; i < combo.length; i++) {
    if (combo[i] === combo[i - 1]) {
      return false;
    }
  }
  return true;
}

Write 100 new XML files

Last step - going through each new combination and creating a new array of measures. Here the first and last measure are always the same as this was a requirement, but you don't have to do this.

Writing the new XML is accomplished by reconverting the modified JS object back to XML.

combinations.forEach((combo) => {
  // first and last measures are always the same
  const last = origMeasures[origMeasures.length - 1];
  const first = origMeasures[0];

  const newMeasures = [first];
  combo.split('').forEach((letter) => {
    patterns[letter].forEach((_, idx) => {
      newMeasures.push(patterns[letter][idx]);
    });
  });
  newMeasures.push(last);

  source.museScore.Score.Staff.Measure = newMeasures;
  source.museScore.Score.Staff.VBox.Text[0].text._text = combo;

  fs.writeFileSync(OUT + combo + '.mscx', convert.js2xml(source, options));
});

The VBox.Text[0].text._text = combo; is optional, it writes the combination as title of the score.

Example result open in MuseScore:

Full code listing is on GitHub.

Batch conversion

At this point it's all done. But we can do one better and generate PDFs to distribute to musicians/students who do not use MuseScore. Thanks to the batch-convert plugin, this is quick and painful.

Lots of formats to chose from! You click OK and point to the out directory where the script wrote all MuseScore XMLs.

And this is it. Now the out/ directory contains 100 MuseScore files and 100 PDFs all named after the random combination of letter motives.

Reusing the script

What if you want to reuse the script for your own purposes, exercises and genarative music? Why, it would give me the most pleasure!

Just clone the github repo, change the Template.mscx and edit the configuration. Then run...

$ node msms.js

... and find a bunch of files in your out/ directory. Then, if required, do the batch conversion to PDF as described above.

New to Node?

A side note for people who think the section above was mostly gibberish. If you're new to Node.js, here are a few more pointers.

  1. Download and install Node from here
  2. Get a copy of the code: go to https://github.com/stoyan/msms and "Download ZIP".

    Unzip where you want. Navigate to that directory.
  3. Install dependencies by running
    $ npm i 
    
  4. Now edit the configuration in msms.js and change the template Template.mscx, then run
    $ node msms.js
    

Print Share Comment Cite Upload Translate
CITATION GOES HERE CITATION GOES HERE
Select a language: