Skip to content

Instantly share code, notes, and snippets.

@HelloWorld017
Last active August 18, 2025 16:36
Show Gist options
  • Select an option

  • Save HelloWorld017/f93758d534b3115115189fe1a924374e to your computer and use it in GitHub Desktop.

Select an option

Save HelloWorld017/f93758d534b3115115189fe1a924374e to your computer and use it in GitHub Desktop.
Convert your Songsterr GuitarPro (*.gp) files suitable for Musescore Editing.
<!DOCTYPE html>
<html>
<head>
<title>Drumsterr</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,[email protected],100..900&display=swap" rel="stylesheet">
<style>
*:where(:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)) {
all: unset;
display: revert;
}
*, *::before, *::after {
box-sizing: border-box;
}
:root {
font-size: 12px;
}
a, button {
cursor: revert;
}
img {
max-inline-size: 100%;
max-block-size: 100%;
}
</style>
<style>
:root {
background: #202020;
}
#app {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100svh;
max-width: 70rem;
margin: 0 auto;
color: #ffffff;
padding: 4rem;
font-family: Inter, system-ui, sans-serif;
letter-spacing: -0.01em;
}
h1 {
font-size: 2.4rem;
font-weight: 700;
margin-bottom: 1.6rem;
letter-spacing: -0.02em;
}
h3 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 1.6rem;
letter-spacing: -0.02em;
}
section {
margin-bottom: 3.6rem;
}
blockquote {
border-left: 3px solid #707070;
padding-left: 2rem;
& p {
font-size: 1.5rem;
line-height: 2rem;
}
}
p {
color: #707070;
font-size: 1.4rem;
line-height: 1.8rem;
}
ol {
margin-left: 2rem;
list-style-type: decimal;
}
li {
font-size: 1.4rem;
line-height: 1.8rem;
margin: 0.4rem 0;
}
code {
background: #20243a;
color: #0080ff;
padding: 0.4rem 0.8rem;
border-radius: 0.8rem;
}
.Dropzone {
border: 1px solid #404040;
border-radius: 1.5rem;
padding: 6rem 3rem;
text-align: center;
transition: all .4s ease;
& i {
display: inline-flex;
font-size: 2.4rem;
padding: 1rem;
margin-bottom: 3rem;
border: 1px solid #404040;
border-radius: 1rem;
}
}
.Dropzone--active {
border-color: #0080ff;
background: #292929;
}
</style>
</head>
<body>
<div id="app">
<h1>Drumsterr</h1>
<section>
<blockquote>
<p>Convert your Songsterr GuitarPro (*.gp) files suitable for Musescore Editing.</p>
</blockquote>
</section>
<h3>Instructions</h3>
<section>
<ol>
<li>Export your score in Songsterr</li>
<li>Drag it to this page</li>
<li>Download converted file and open it in Musescore</li>
<li>Go to layout tab in your sidebar, click the config icon, and click <code>Replace instrument</code></li>
<li>Find <code>Drum kit (common)</code>.</li>
<li>
To remove accidentals (which are shown by the Musescore bug), follow these steps.
<ol>
<li>click an accidental, click <code>Select > Similar</code></li>
<li>Go to properties tab in your sidebar, uncheck the <code>visible</code> checkbox.</li>
</ol>
</li>
</ol>
</section>
<h3>Converter</h3>
<div id="input"></div>
</div>
<script type="module">
import { createRoot } from 'https://esm.sh/[email protected]/client';
import { html } from 'https://esm.sh/[email protected]/react';
import { useCallback } from 'https://esm.sh/[email protected]';
import { useDropzone } from 'https://esm.sh/[email protected][email protected]';
import JSZip from 'https://esm.sh/[email protected]';
import { XMLParser, XMLBuilder } from 'https://esm.sh/[email protected]';
// TODO add elements remapping
const DRUM_LOOKUP = {
Bass: 36,
Snare: 38,
SnareRim: 37,
Cowbell: 56,
CymbalsCrash1: 49,
CymbalsCrash2: 57,
CymbalsRide1: 51,
CymbalsRide2: 59,
CymbalsRideCup: 53,
CymbalsSplash: 55,
HiHatClosed: 42,
HiHatOpened: 46,
HiHatPedal: 44,
TomsLow: 45,
TomsMidLow: 47,
TomsMid: 48,
TomsHigh: 50,
TomsFloor: 43,
TomsFloorLow: 41,
};
const opts = {
attributeNamePrefix: '',
ignoreAttributes: false,
preserveOrder: true
};
const parser = new XMLParser(opts);
const builder = new XMLBuilder(opts);
const alterNode = (target, tagName, originalNode, transformedNode) => {
const index = target?.findIndex(item => Object.hasOwn(item, tagName) && item[tagName] === originalNode);
return target?.toSpliced(index, index < 0 ? 0 : 1, { [tagName]: transformedNode });
};
const findByTagName = (target, tagName) =>
target?.find(item => Object.hasOwn(item, tagName))?.[tagName];
const findByAttribute = (target, tagName, [attrName, attrValue]) =>
target?.find(item => Object.hasOwn(item, tagName) && item[':@']?.[attrName] === attrValue)?.[tagName];
const isDrumNote = properties => {
const pitch = findByTagName(findByAttribute(properties, 'Property', ['name', 'ConcertPitch']), 'Pitch');
const step = findByTagName(findByTagName(pitch, 'Step'), '#text');
const octave = findByTagName(findByTagName(pitch, 'Octave'), '#text');
return step === 'C' && octave === -1;
};
const addMIDIProperty = properties => {
const midiProperty = findByAttribute(properties, 'Property', ['name', 'Midi']);
if (midiProperty) return properties;
const fretProperty = findByAttribute(properties, 'Property', ['name', 'Fret']);
const fret = findByTagName(findByTagName(fretProperty, 'Fret'), '#text');
return [
...properties,
{
':@': { name: 'Midi' },
Property: [
{ Number: [{ '#text': fret }] }
]
}
];
};
const transform = root => {
const gpif = findByTagName(root, 'GPIF');
const notes = findByTagName(gpif, 'Notes');
const notesTransformed = notes.map(note => {
if (!Object.hasOwn(note, 'Note')) return note;
const properties = findByTagName(note.Note, 'Properties');
if (!isDrumNote(properties)) return note;
const propertiesTransformed = addMIDIProperty(properties);
const noteTransformed = alterNode(note.Note, 'Properties', properties, propertiesTransformed);
return { ...note, Note: noteTransformed };
});
const gpifTransformed = alterNode(gpif, 'Notes', notes, notesTransformed);
const rootTransformed = alterNode(root, 'GPIF', gpif, gpifTransformed);
return rootTransformed;
};
const download = (url, name) => {
const a = document.createElement('a');
a.href = url;
a.download = name;
a.click();
};
const Input = () => {
const onDrop = useCallback(async ([file]) => {
const zip = await JSZip.loadAsync(file);
const content = await zip.file('Content/score.gpif').async('string');
const contentObject = parser.parse(content);
const outputBlob = await zip.file('Content/score.gpif', builder.build(transform(contentObject)))
.generateAsync({ type: 'blob' });
download(URL.createObjectURL(outputBlob), file.name.replace(/\.gp$/, '-drumsterr.gp'));
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
return html`
<div ...${getRootProps()} class="${`Dropzone${ isDragActive ? ' Dropzone--active' : ''}`}">
<input ...${getInputProps()} />
<i>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m2 2 8 8"/>
<path d="m22 2-8 8"/>
<ellipse cx="12" cy="9" rx="10" ry="5"/>
<path d="M7 13.4v7.9"/>
<path d="M12 14v8"/>
<path d="M17 13.4v7.9"/>
<path d="M2 9v8a10 5 0 0 0 20 0V9"/>
</svg>
</i>
<h3>Click or Drag & Drop</h3>
<p>
Drop a Songsterr GuitarPro file here.<br />
The file will not be uploaded and processed in your device.
</p>
</div>
`;
};
createRoot(document.querySelector('#input')).render(html`<${Input} />`);
</script>
</body>
</html>
@HelloWorld017
Copy link
Author

HelloWorld017 commented Aug 18, 2025

I have fixed it and now you can use it with updated link :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment