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 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
- Create a MuseScore file to be used as a "template"
- Reshuffle the measures by manipulating XML in a Node.js script and spit out an N number of different MuseScore files
- 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:
- Read the XML template using xml-js into a JavaScript object for manipulation
- Extract the motive measures from the XML
- Generate 100 random permutations of the desired motives and their quantity
- 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.
- Download and install Node from here
- Get a copy of the code: go to https://github.com/stoyan/msms and "Download ZIP".
Unzip where you want. Navigate to that directory. - Install dependencies by running
$ npm i
- Now edit the configuration in
msms.js
and change the templateTemplate.mscx
, then run$ node msms.js