Sheet music creation with JavaScript

I did this as part of my Master’s Degree thesis. It is a pretty old and clumsy implementation (I admit it) and I should revise and re-write it with some modern JS framework, following the good practices and principles, etc.

Nevertheless, my idea was to create a unified platform for every musician, where he/she can find everything he/she needs to be able to practice and to have fun without the need to download various apps, buy different gadgets or browse multiple websites.

We are going to discuss just one of the functionalities of this platform.
I am going to share with you one idea, one possible way of creating music notations online.

Toolbox

What we need to build this app:

  • JavaScript
  • abcjs – JS library
  • imagination

abcjs

Let’s have some words about that thing.

abcjs is an open-source JavaScript music notation library for rendering standard music notation in a browser.

It actually creates and visualizes the music sheet with all its elements like clefs, notes, breaks, accidentials, and most of the existing signatures which exist.
Furthermore, it can be used to display alternate heads and chords.

abcjs can also help with the export of the created music into a midi file.

The complete documentation can be found HERE.

And this is the GitHub page.

Implementation

I’ve built this as a .NET Framework, MVC project but the whole “meet” is actually in the JavaScript files.
So you can implement it using the technology of your choice.

The View

Define the notation creation container with the appropriate tabs as partial views:

<div class="tab-content" id="sheetTabContent">
        @Html.Partial("_GeneralTab")
        @Html.Partial("_NotesTab")
        @Html.Partial("_OthersTab")
        @Html.Partial("_AbcTab")
</div>

And some other actions like a button to show the indexes where the notes are situated (this is needed if you want to remove a note at a certain position by specifying its index), the download sheet button, and the container for the actual notation representation:

<a class="btn btn-link" id="showIndexes">Show indexes</a>

<a class="btn btn-primary btn-xs" id="exportToPdf" href="#">
   Download sheet
</a>

<div id="midi" class="btn btn-primary btn-xs"></div>
<div id="musicSheetCanvas"></div>

Here are the partial views.
The General tab:

@using SheetMusic.Hepers

<div id="general" class="tab-pane fade active in">
    <div class="row" id="clefsContainer">
        <div class="col-small">
            <div class="float-left label-div">
                <span class="label label-primary">Clef:</span>
            </div>
            <label>
                <input type="radio" value="optionG" id="optG" name="optionsClef" checked>
                <img height="40" width="40" src="~/Resources/Images/clef-G.png" alt="clef-G" />
            </label>
            <label>
                <input type="radio" value="optionF" id="optF" name="optionsClef">
                <img height="40" width="40" src="~/Resources/Images/clef-F.png" alt="clef-F" />
            </label>
            <label>
                <input type="radio" value="optionC" id="optC" name="optionsClef">
                <img height="40" width="40" src="~/Resources/Images/clef-C.png" alt="clef-C" />
            </label>
        </div>
    </div>
    <div class="row">
        <div class="col-small">
            <div class="float-left label-div">
                <span class="label label-primary">Metrics:</span>
            </div>
        </div>
        <div class="float-left">
            <div>Beats per bar</div>
            <div>Beat unit</div>
        </div>
        <div id="metricsContainer" class="col-small">
            <div><input type="text" class="txt-square metrics-count" value="4" /></div>
            <div><input type="text" class="txt-square metrics-unit" value="4" /></div>
        </div>
    </div>
    <div class="row">
        <div class="col-small">
            <div class="float-left label-div">
                <span class="label label-primary">Key: </span>
            </div>
            @Html.DropDownList("Keys", Html.MusicSheetKeys(), new { @Class = "btn-default padding5" })
        </div>
    </div>
    <div class="row">
        <div class="col-small">
            <div class="float-left label-div">
                <span class="label label-primary">Title: </span>
            </div>
            <input type="text" id="sheetTitle" />
        </div>
    </div>
</div>

Notes:

<div id="notes" class="tab-pane fade">
    <ul class="nav nav-pills nav-stacked float-left">
        <li id="notesTimeTab" class="active disabled"><a class="disabled-link" data-toggle="tab" href="#time" aria-expanded="true">Time</a></li>
        <li id="notesNoteTab" class="disabled"><a class="disabled-link" data-toggle="tab" href="#note" aria-expanded="false">Note</a></li>
        <li id="notesOctaveTab" class="disabled"><a class="disabled-link" data-toggle="tab" href="#octave" aria-expanded="false">Octave</a></li>
    </ul>
    <div class="tab-content" id="notesTabContent">
        @Html.Partial("_NotesTab_Time")
        @Html.Partial("_NotesTab_Note")
        @Html.Partial("_NotesTab_Octave")
    </div>
</div>

Others:

<div id="others" class="tab-pane fade">
    <div class="btn-toolbar breaks-add">
        <span class="label label-primary">Breaks:</span>
        <div class="btn-group" role="group">
            <a class="btn btn-default med-size" id="optBreakOne">
                <img class="others-tab-images" id="optBreakOne" src="~/Resources/Images/one.jpg" alt="one" />
            </a>
            <a class="btn btn-default med-size" id="optBreakHalf">
                <img class="others-tab-images" id="optBreakHalf" height="40" width="40" src="~/Resources/Images/1.2.jpg" alt="half" />
            </a>
            <a class="btn btn-default med-size" id="optBreakFourth">
                <img class="others-tab-images" id="optBreakFourth" height="40" width="40" src="~/Resources/Images/1.4.jpg" alt="fourth" />
            </a>
            <a class="btn btn-default med-size" id="optBreakEight">
                <img class="others-tab-images" id="optBreakEight" height="40" width="40" src="~/Resources/Images/1-8.jpg" alt="eight" />
            </a>
            <a class="btn btn-default med-size" id="optBreakSixteenth">
                <img class="others-tab-images" id="optBreakSixteenth" height="40" width="40" src="~/Resources/Images/1-16.jpg" alt="sixteenth" />
            </a>
            <a class="btn btn-default med-size" id="optBreakThirtysecond">
                <img class="others-tab-images" id="optBreakThirtysecond" height="40" width="40" src="~/Resources/Images/1-32.jpg" alt="thirtysecond" />
            </a>
        </div>
    </div>
    <span class="label label-primary">Others:</span>
    <div class="btn-toolbar repeat-add">
        <div class="btn-group" role="group">
            <a class="btn btn-default med-size" id="optRepeatBegin">
                <img class="others-tab-images" id="optRepeatBegin" height="40" width="40" src="~/Resources/Images/repeat_begin.png" alt="one" />
            </a>
            <a class="btn btn-default med-size" id="optRepeatEnd">
                <img class="others-tab-images" id="optRepeatEnd" height="40" width="40" src="~/Resources/Images/repeat_end.png" alt="half" />
            </a>
            <a class="btn btn-default med-size" id="optLine">
                <img class="others-tab-images" id="optLine" height="40" width="40" src="~/Resources/Images/Line.png" alt="fourth" />
            </a>
        </div>
    </div>
    <div class="row others-add">
        <div class="col-small">
            <div class="float-left label-div">
                <span class="label label-primary">Line: </span>
            </div>
            <a id="addLine" href="#">Add new</a>
        </div>
    </div>
    <div class="row">
        <div class="col-small">
            <div class="float-left label-div">
                <span class="label label-primary">Delete: </span>
            </div>
            <span class="align-bottom">
                <a id="deleteElement" href="#"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a>
            </span>
        </div>
        <div class="col-small float-padding-right">
            at index
            <input type="text" id="deleteAddIndex" class="txt-small" />
        </div>
    </div>
</div>

And there is the ABC tab, where the user can directly type the ABCJS notation text based on the ABC standards and not using the custom user-friendly interface. So this is not of interest to us now.

We aim to have the following result:

Now we are ready to start with the JS.
The scripts can be found under Scripts/Custom/ folder in the project.

First, take a look at the common.js file where we just mark as active the clicked tab.

(function (musicassistant, $) {
    "use strict"

    musicassistant.common = (function () {

        var init = function () {
            $(function () { 
                $('a[href="' + location.pathname + '"]').parent().addClass('active');
            });
        }

        return {
            init: init
        }
    })();

    musicassistant.common.init();

})(window.musicassistant = window.musicassistant || {}, window.jQuery);

*I have used namespaces for the overall implementation

globals.js contains some constants and helper methods:

(function (musicassistant, $) {

    musicassistant.globals = (function () {
        var delimiter = ':',
            elements = [],
            copiedElements = [],

            generals = {
                x: { key: 'X', val: '1' }, // Reference number
                m: { key: 'M', val: '4/4' },  // Meter
                k: { key: 'K', val: 'C' },  // Key
                c: { key: 'K', val: 'clef: treble' },
                l: { key: 'L', val: '1/8' },  // Unit note length
                t: { key: 'T', val: $('#sheetTitle').val() } // Title
            },

            addElement = function (el) {
                elements.push(el);
            },

            addElementAtIndex = function (el, index) {
                if (index > -1)
                    elements.splice(index, 0, el);
            },

            sheet = function () {
                var sheetStr = (
                    getSheetProp(generals.x.key, generals.x.val) + "\n" +
                    getSheetProp(generals.m.key, generals.m.val) + "\n" +
                    getSheetProp(generals.k.key, generals.k.val) + "\n" +
                    getSheetProp(generals.c.key, generals.c.val) + "\n" +
                    getSheetProp(generals.l.key, generals.l.val) + "\n" +
                    getSheetProp(generals.t.key, generals.t.val) + "\n" +
                    elements.join('')
                );

                return sheetStr;
            },

            getSheetProp = function (a, b) {
                return a.concat(delimiter).concat(b);
            },

            showIndexes = function () {
                copiedElements = elements;
                elements = [];

                var counter = 0;

                for (i = 0; i < copiedElements.length; i++) {
                    elements.push('"' + counter + '"' + copiedElements[i]);

                    counter = counter + 1;
                }
            },

            updateElements = function () {
                elements = copiedElements;
                copiedElements = [];
            }

        return {
            generals: generals,
            sheet: sheet,
            delimiter: delimiter,
            getSheetProp: getSheetProp,
            elements: elements,
            addElement: addElement,
            showIndexes: showIndexes,
            updateElements: updateElements,
            addElementAtIndex: addElementAtIndex
        };
    })();

})(window.musicassistant = window.musicassistant || {}, window.jQuery);

The main implementation and integration with ABCJS library are happening in music-sheet.js.

Loading the editor:

// using abcjs Editor 
        loadEditor = function () {
            new ABCJS.Editor("txtAbc", {
                canvas_id: "musicSheetCanvas",
                midi_id: "midi",
                warnings_id: "warnings",
                midi_options: {
                    program: 1,
                    qpm: 150,
                    type: "qt"
                }
            });
        },

Let’s define the times and cleffs representations:

timesRepresentation = {
            "timeFullNote": "8",
            "timeHalfNote": "4",
            "timeFourthNote": "2",
            "timeEightNote": "",
            "timeSixteenthNote": "1/2",
            "timeThirtysecondNote": "1/4",
            "timeFullNoteDot": "9",
            "timeHalfNoteDot": "5",
            "timeFourthNoteDot": "3",
            "timeEightNoteDot": "7/5",
            "timeSixteenthNoteDot": "2/3",
            "timeThirtysecondNoteDot": "1/3",
            "default": ""
        },

        clefsRepresentation = {
            "optG": "clef: treble",
            "optF": "clef: bass",
            "optC": "clef: alto"
        },

The sheet title:

 setSheetTitle = function () {
            globals.generals.t.val = $('#sheetTitle').val();

            reloadSheet();
        },

We have a reusable method that defines the current element:

setCurrentElement = function () {
            var octavesRepresentation = {
                "1": noteNote.toUpperCase().concat(","), // Capitalize
                "2": noteNote.toUpperCase(), // Capitalize
                "3": noteNote.toLowerCase(), // Lowerize
                "4": noteNote.toLowerCase().concat("'"), // Lowerize
                "default": ""
            };

            currentElement = octavesRepresentation[noteOctave] + timesRepresentation[noteTime];
        },

Let’s have functions for keeping the selected items – octave, note, and time:

timeSelected = function (e) {
            // store the selected value
            noteTime = e.target.id;

            // open the next tab
            $('.nav-pills > .active').next('li').find('a').trigger('click');
        },

        noteSelected = function (e) {
            // store the selected value
            noteNote = e.target.id;

            // open the next tab
            $('.nav-pills > .active').next('li').find('a').trigger('click');
        },

        clearOctaveCss = function () {
            $('#octave #1').removeClass('btn-primary');
            $('#octave #2').removeClass('btn-primary');
            $('#octave #3').removeClass('btn-primary');
            $('#octave #4').removeClass('btn-primary');
        },

        octaveSelected = function (e) {
            // store the selected value
            noteOctave = e.target.id;

            // clear the unneeded styles from the buttons
            clearOctaveCss();

            // mark the selected button
            $(e.target).addClass('btn-primary');

            // show the functions section
            $('#sheet-functions').removeClass('hidden');
        },

Based on the user’s choice there are methods for adding a selected note either to the sheet or remembering it as a note for a chord (if we want to create a chord, containing several notes):

        addToSheet = function () {
            var indexToAdd = $('#updateIndex');

            if (indexToAdd.val() == '')
                globals.addElement(currentElement);
            else
                globals.addElementAtIndex(currentElement, indexToAdd.val());

            indexToAdd.val('');
            reloadSheet();
        },
        addNoteToSheet = function () {
            setCurrentElement();
            addToSheet();
            currentElement = '';

            // clear the unneeded styles from the buttons
            clearOctaveCss();

            // hide the functions section
            $('#sheet-functions').addClass('hidden');

            $('#notesTimeTab').find('a').trigger('click');
        },

        addNoteToChord = function () {
            $('#liFinishChord').removeClass('hidden');

            currentChord = currentElement;
            setCurrentElement();

            currentElement = currentChord.concat(currentElement);
        },

        addToChord = function () {
            addNoteToChord();
 

            // clear the unneeded styles from the buttons
            clearOctaveCss();
            $('#addToSheet').addClass('disabled-link opacity-half');

            // hide the functions section
            $('#sheet-functions').addClass('hidden');

            $('#notesNoteTab').find('a').trigger('click');
        },

To finish the chord:

finishChord = function () {
            addNoteToChord();
            currentElement = '[' + currentElement.concat(']');
            addToSheet();
            currentElement = '';
            currentChord = '';

            // clear the unneeded styles from the buttons
            clearOctaveCss();
            $('#addToSheet').removeClass('disabled-link opacity-half');
            $('#liFinishChord').addClass('hidden');

            // hide the functions section
            $('#sheet-functions').addClass('hidden');

            $('#notesTimeTab').find('a').trigger('click');
        },

To handle the deletion, we would need to have the index of the selected note or other elements to be removed.

reloadSheet = function () {
            $('#txtAbc').val(globals.sheet);
            loadEditor();
        },

 deleteElement = function () {
            var indexToRemove = $('#deleteAddIndex');
            if (indexToRemove.val() == '')
                globals.elements.pop();
            else
                globals.elements.splice(indexToRemove.val(), 1);

            indexToRemove.val('');
            reloadSheet();
        },

Change functionality for the clef, the key, and the metrics:

        changeClef = function (e) {
            globals.generals.c.val = clefsRepresentation[e.target.id];
            reloadSheet();
        },

        changeKey = function (e) {
            globals.generals.k.val = e.target.value;
            reloadSheet();
        },

        changeMetrics = function () {
            var beatsPerBar = $('.metrics-count').val();
            var beatsUnit = $('.metrics-unit').val();

            if (beatsPerBar != '' && beatsPerBar != 'undefined' && beatsUnit != '' && beatsUnit != 'undefined') {
                globals.generals.m.val = beatsPerBar + '/' + beatsUnit;
                reloadSheet();
            }
        },

Take a look at the final result:

Twinkle twinkle little star

Resources

Sheet Music Creator: You can find the complete code base HERE:
https://github.com/svilenp/SheetMusic

absjs:
– website: https://www.abcjs.net/
– source code: https://github.com/paulrosen/abcjs
– documentation: https://paulrosen.github.io/abcjs/

Leave a comment