Author Topic: How to use an external music editor  (Read 741 times)

firiban

  • Traveller
  • *
  • Posts: 27
    • View Profile
How to use an external music editor
« on: December 26, 2011, 01:20:24 pm »
Hi there!

Since the ingame notation editor is somewhat painful to use, i wrote a script which converts standard MusicXML to a PlaneShift compatible format (PS uses a subset of MusicXML). I'm not claiming this will work for everyone with every MusicXML file, but it works for me and i thought i'd share it.

-- Edit: There's now an easier method that does not require node.js, see below --

1) Software needed
First, you need a notation editor which can write MusicXML files. Personally, i use MuseScore. Then, you'll need node.js and the jsdom library. On Linux, you can install jsdom with
Code: [Select]
npm install jsdomafter installing node.js. Node's npm package manager is kind of weird and i haven't completely figured out when node considers something to be installed and when not :|

2) The script
Copy the following into a file called converter.js in your musicalsheets directory (on Linux it's ~/.PlaneShift/musicalsheets/)
Code: [Select]
#!/usr/bin/env node

var jsdom = require("jsdom");
var fs = require('fs');


console.log("MusicXML to PlaneShift converter by Firiban - version 0.2");
console.log("parsing file "+process.argv[2]+"...");
// read file, parse it, create outfile with header
var infile = jsdom.jsdom(fs.readFileSync(process.argv[2], "utf8"));
var outfile = '<score-partwise version="2.0"><part-list><score-part id="P1"><part-name>'+process.argv[2].substr(0,process.argv[2].length-4)+'</part-name></score-part></part-list><part id="P1">';
var measures = infile.getElementsByTagName("measure");
console.log("Sorting the stuff...");

// This loop does the work: Iterates through the measures and copies the important stuff (read: "PlaneShift compatible tags") to outfile
var i=1;
var j=0;
var k=0;
var l=0;
var alter=0;
var duration=1;
var multiplier=1;
for (var i=0;i<measures.length;i++) {
outfile += '<measure number="'+(i+1)+'">';
if (i==0) {
multiplier = (4 / infile.getElementsByTagName("divisions")[0].childNodes[0].__nodeValue);
outfile += '<attributes><divisions>4</divisions><key><fifths>'+((infile.getElementsByTagName("fifths").length>0)?infile.getElementsByTagName("fifths")[0].childNodes[0].__nodeValue:"0")+'</fifths></key><time><beats>'+infile.getElementsByTagName("beats")[0].childNodes[0].__nodeValue+'</beats><beat-type>'+infile.getElementsByTagName("beat-type")[0].childNodes[0].__nodeValue+'</beat-type></time></attributes><direction>';
if (infile.getElementsByTagName("sound").length>0) {
outfile += '<sound tempo="'+infile.getElementsByTagName("sound")[0].attributes[0].value+'"/>';
} else {
outfile += '<sound tempo="90"/>';
}
outfile += "</direction>";
}
//console.log(measures[i]);
for (j=0;j<measures[i].children.length;j++) {
// we only need the notes, no stupid <print> stuff or text nodes (whitespaces/tabs/newlines etc.)
if (measures[i].children[j].tagName=="NOTE") {
outfile +="<note>";
duration=1;
// iterate through note's parameters and copy the neccessary ones (sort out <stem> etc., since they are not used by PS)
for (k=0;k<measures[i].children[j].children.length;k++) {
if (measures[i].children[j].children[k].tagName=="REST") {
outfile += "<rest/>";
} else if (measures[i].children[j].children[k].tagName=="CHORD") {
outfile += "<chord/>";
} else if (measures[i].children[j].children[k].tagName=="DURATION") {
duration = measures[i].children[j].children[k].textContent;
} else if (measures[i].children[j].children[k].tagName=="PITCH") {
outfile += "<pitch>";
alter = 0;
for (l=0;l<measures[i].children[j].children[k].children.length;l++) {
if ((measures[i].children[j].children[k].children[l].tagName=='STEP') || (measures[i].children[j].children[k].children[l].tagName=='OCTAVE')) {
outfile += "<"+measures[i].children[j].children[k].children[l].tagName.toLowerCase()+">"+measures[i].children[j].children[k].children[l].textContent+"</"+measures[i].children[j].children[k].children[l].tagName.toLowerCase()+">";
} else if (measures[i].children[j].children[k].children[l].tagName=='ALTER') {
alter = measures[i].children[j].children[k].children[l].textContent;
}
}
outfile += "<alter>"+alter+"</alter></pitch>";
}
}
outfile += "<duration>"+duration*multiplier+"</duration>";
outfile +="</note>";
}
}
outfile += '</measure>';
}

// close outfile and write it
outfile += '</part></score-partwise>';
fs.writeFileSync("PS-"+process.argv[2],outfile);
console.log("Done. Your PlaneShift-compatible music is now in PS-"+process.argv[2]+" . Enjoy! :)");
Save the file and make it executable. (chmod +x ./converter.js in a terminal)

3) Write some music
Well, open MuseScore and create a new sheet with one voice. Use flute or guitar or some other non-transposing one-staff instrument. There are some things which are not supported by PlaneShift, so you shouldn't use them in your composition:
  • Polyrhythm (e.g. whole note chord with quarter note melody above it)
  • I haven't tested key signatures different from C major/A minor (but single #s and bs should work)
  • tied notes
Be sure there's a time signature at the beginning of your piece, otherwise the script will fail.
You may add a tempo definition, otherwise, it will use 90BPM.

4) Make your song PlaneShift compatible
Save your composition as MusicXML (not compressed MusicXML) in your musicalsheets directory. Then let the script convert it. On Linux, open in a terminal and do
Code: [Select]
./converter.js yoursong.xml(converter.js and yoursong.xml should both be in musicalsheets, and yoursong.xml should be the filename you used for your song :P)
You'll now get a file called PS-song.xml which you can put in your musicalsheets directory and open with the ingame editor. Have fun! :)


Known Bugs: In the ingame editor, only the first measure of the converted piece is shown, but everything is played. If anyone can tell me how to fix this, please do so.


If you have any questions, please post them here (though i might not be able to help you, depending on your problem :P).

Happy composing :)
Firiban


Easier method

1) Create a file called converter.html with the following content
Code: [Select]
<html>
<head>
</head>
<body>
<h1>MusicXML to PlaneShift converter by Firiban - version 0.2</h1>
<form name="convert" action="#" method="get">
<textarea name="code" cols="100" rows="30"></textarea>
<br />
<input type="button" value="Convert!" name="startme" />
</form>
<script type="application/x-javascript">
// NOTE: This is just the web browser compatible port of the original converter.js
document.forms.convert.startme.onclick = function () {
console.log("MusicXML to PlaneShift converter by Firiban - version 0.2");
console.log("parsing file input...");
// read file, parse it, create outfile with header
if (window.DOMParser) {
var infile = (new DOMParser()).parseFromString(document.forms.convert.code.value,"text/xml");
}
else {
alert ("Incompatible Browser. Try Firefox.");

var outfile = '<score-partwise version="2.0"><part-list><score-part id="P1"><part-name>converted stuff</part-name></score-part></part-list><part id="P1">';
var measures = infile.getElementsByTagName("measure");
console.log("Sorting the stuff...");

// This loop does the work: Iterates through the measures and copies the important stuff (read: "PlaneShift compatible tags") to outfile
var i=1;
var j=0;
var k=0;
var l=0;
var alter=0;
var duration=1;
var multiplier=1;
for (var i=0;i<measures.length;i++) {
outfile += '<measure number="'+(i+1)+'">';
if (i==0) {
multiplier = (4 / infile.getElementsByTagName("divisions")[0].childNodes[0].textContent);
outfile += '<attributes><divisions>4</divisions><key><fifths>'+((infile.getElementsByTagName("fifths").length>0)?infile.getElementsByTagName("fifths")[0].childNodes[0].__nodeValue:"0")+'</fifths></key><time><beats>'+infile.getElementsByTagName("beats")[0].childNodes[0].__nodeValue+'</beats><beat-type>'+infile.getElementsByTagName("beat-type")[0].childNodes[0].__nodeValue+'</beat-type></time></attributes><direction>';
if (infile.getElementsByTagName("sound").length>0) {
outfile += '<sound tempo="'+infile.getElementsByTagName("sound")[0].attributes[0].value+'"/>';
} else {
outfile += '<sound tempo="90"/>';
}
outfile += "</direction>";
}
//console.log(measures[i]);
for (j=0;j<measures[i].children.length;j++) {
// we only need the notes, no stupid <print> stuff or text nodes (whitespaces/tabs/newlines etc.)
if (measures[i].children[j].tagName=="note") {
outfile +="<note>";
duration=1;
// iterate through note's parameters and copy the neccessary ones (sort out <stem> etc., since they are not used by PS)
for (k=0;k<measures[i].children[j].children.length;k++) {
if (measures[i].children[j].children[k].tagName=="rest") {
outfile += "<rest/>";
} else if (measures[i].children[j].children[k].tagName=="chord") {
outfile += "<chord/>";
} else if (measures[i].children[j].children[k].tagName=="duration") {
duration = measures[i].children[j].children[k].textContent;
} else if (measures[i].children[j].children[k].tagName=="pitch") {
outfile += "<pitch>";
alter = 0;
for (l=0;l<measures[i].children[j].children[k].children.length;l++) {
if ((measures[i].children[j].children[k].children[l].tagName=='step') || (measures[i].children[j].children[k].children[l].tagName=='octave')) {
outfile += "<"+measures[i].children[j].children[k].children[l].tagName+">"+measures[i].children[j].children[k].children[l].textContent+"</"+measures[i].children[j].children[k].children[l].tagName+">";
} else if (measures[i].children[j].children[k].children[l].tagName=='alter') {
alter = measures[i].children[j].children[k].children[l].textContent;
}
}
outfile += "<alter>"+alter+"</alter></pitch>";
}
}
outfile += "<duration>"+duration*multiplier+"</duration>";
outfile +="</note>";
}
}
outfile += '</measure>';
}

// close outfile and write it
outfile += '</part></score-partwise>';
document.forms.convert.code.value = outfile;
console.log("Done. Enjoy! :)");
}
</script>
</body>
</html>

2) open that file with your web browser. paste the content of your musicxml file there, click convert.

3) save the resulting code to an xml file in your musicalsheets folder and load it with planeshift.


Edit #1: added html method
Edit #2: Fixed bug with accidentals
Edit #3: Added support for adding missing <fifths> tag; now supports Rosegarden
« Last Edit: December 26, 2011, 04:45:21 pm by firiban »
Code: [Select]
    if (brain)
    {
        delete brain;
    }
From PlaneShift's npcclient code.

Gilrond

  • Hydlaa Notable
  • *
  • Posts: 764
    • View Profile
Re: How to use an external music editor
« Reply #1 on: December 26, 2011, 02:50:48 pm »
You can avoid converters (or manage it with few manual tweaks) if you stick to few limitations discussed previously here:
http://www.hydlaaplaza.com/smf/index.php?topic=40219.15
Writing a universal converter is probably ambiguous, since each editor has its own quirks (I used Rosegarden for example). The best thing you can do is to write a converter for a particular editor, but that won't satisfy all users. So if this works for MuseScore - that's good.

Besides tied notes, stick to C major tonality in resulting file. PS mistakenly shifts actual playing if tonality is set to something different in the beginning. As you already mentioned, multiple voices and parts are not supported either. I'm not sure why you are getting the first measure only, I get a full song. I suspect you didn't conform to PS limitations on divisions and durations usage. See the linked thread for details if you didn't review it earlier.
« Last Edit: December 26, 2011, 03:02:10 pm by Gilrond »

firiban

  • Traveller
  • *
  • Posts: 27
    • View Profile
Re: How to use an external music editor
« Reply #2 on: December 26, 2011, 03:42:44 pm »
You're right, this is not compatible with every editor, but personally i still prefer MuseScore over the ingame editor, since it supports midi keyboard input and is far less buggy and more intuitive. Also, Musescore can import midi files and convert them and so on.
I had a look at rosegarden, and it seems that you have to add
Code: [Select]
<fifths>0</fifths>to the <attributes> section manually in the original musicxml file to make the converter work.
Code: [Select]
    if (brain)
    {
        delete brain;
    }
From PlaneShift's npcclient code.

Gilrond

  • Hydlaa Notable
  • *
  • Posts: 764
    • View Profile
Re: How to use an external music editor
« Reply #3 on: December 26, 2011, 04:25:32 pm »
Also, why in JavaScript, just out of preference? Ruby or Python would be probably easier. I didn't test MuseScore, but Rosegarden supports all those features you mentioned as well (Rosegarden seems to be more feature packed).  But MuseScore also runs on other OSes while Rosegarden is Linux only, so this can be useful for non Linux users.
« Last Edit: December 26, 2011, 04:32:24 pm by Gilrond »

firiban

  • Traveller
  • *
  • Posts: 27
    • View Profile
Re: How to use an external music editor
« Reply #4 on: December 26, 2011, 04:34:34 pm »
Yes, i used javascript because it's the language i'm best at, it's cross-platform (node+web, different browsers etc) and i used that DOM API a thousand times in web-related stuff. I know Rosegarden can do the converting as well, i just mentioned it to compare musescore to the ingame editor.

Edit: I updated the script, it should now support files created with Rosegarden.
« Last Edit: December 26, 2011, 04:45:57 pm by firiban »
Code: [Select]
    if (brain)
    {
        delete brain;
    }
From PlaneShift's npcclient code.

Korumak

  • Hydlaa Resident
  • *
  • Posts: 58
    • View Profile
Re: How to use an external music editor
« Reply #5 on: January 11, 2012, 09:58:37 pm »
Thought I would give a tag in on this one.
I used the HTML page version, the other one well... didn't want to install Sun's Java, the other one worked with Open Java.
It does work.  Only gripe it does flat out remove everything but the melody line.  But compared to nothing at all I'll happily take it.

Just waiting on the Dev's to fix the music system now and back to barding with out starting a riot  ;D

Great work by the way  :thumbup:

"You can never have too many cat girls!" and you can quote me on that! 

Gilrond

  • Hydlaa Notable
  • *
  • Posts: 764
    • View Profile
Re: How to use an external music editor
« Reply #6 on: January 11, 2012, 10:14:24 pm »
Manual fix with few regex substitutions works more or less as well if you know how to do it of course. Just don't forget the tonality patch (to C major) and adding all missing <alter>0</alter>.