Pages

Men

rh

10/29/2014

HTML5 Guitar Tab Player


Html5 Tab Player
Figure 1. The HTML5 Tab Player - Beatles Demo Page

Introduction

If you ever played guitar, you probably used tablatures (also called "tabs") sometimes. And if you searched for tabs on the internet, most of the times you must have found the same kind of plain text tablature.
Some websites provide tab players, but in most cases you will have to register a paid, premium account before using the service. And in most cases this service will run in Flash.
But what if you could take those plain text tabs and play them right now? If you like the idea, then this article is for you. HTML5 Tab Player is a jQuery plugin that allows you to easily embed guitar tabs in your web pages, without relying on Flash plug-ins.

Background

For me, combining music and programming in a single article is a dream coming true. Programming is the one that pays the bills, but lately I've been dedicating more of my precious weekend hours to play acoustic guitar.
Figure 2. Hey, that's Bob!
The idea of the HTML5 Tab Player jQuery plugin came to me when I was looking for guitar tabs for specific songs on the internet, and suddenly I realized that all of them were plain text. Next I remembered that most programming blogs and programmer-dedicated websides such as Code Project use formatting components that parse and convert plain text code snippets submitted by contributors into more user-readable, well formatted HTML content. That is, the formatting takes into consideration the semantics of the programming language being presented, and then all the keywords of that language are highlighted or coloured accordingly, without need of human intervention.

System Dependencies

The HTML5 Tab Player plug-in runs entirely on the client side, and deploying the accompanying files is enough to get it running:
  • tabplayer.css: the style sheet where you can customize the HTML5 Tab Player appearance.
  • img folder: contains the images for the HTML5 Tab Player toolbar.
  • jQuery-1.9.1.min.js: the jQuery on top of which the plug-in code is built.
  • tabplayer.js: contains all the plug-in functionalities.
  • sounds folder: contains all sound files, each one representing one distinct acoustic guitar note.

Figure 3. File dependencies

What Does The Plugin Do (And What it Does Not)

As stated before, idea for this plug-in crossed my mind as I observed websites (like codeproject.com) that format plain code into well formated, highligted HTML content. Take a look, for example, at Prettify:
Figure 4. Prettify: plain code goes in, pretty code comes out
If you think that most guitar tabs found on the internet are just plain text, and that they are contained inside <pre> tags, they can be seen as code waiting to be formatted. The example below was taken from ultimate-guitar.com:
Figure 5. The Ultimate Guitar Tab website always shows tabs inside <pre> tags
As input, our HTML5 Tab Player takes the following tablature in a <pre> tag. Notice that it requires thelang=tabPlayer attribute. Other attributes, such as bartime and class will be explained later.
Figure 6. The unformatted HTML Pre Tag
As a result, the HTML5 Tab Player formats the plain text tablature, hightlighting the notes numbers and creating a toolbar panel just before the tab, which allows the user to play, pause, stop and configure the player.
Figure 7. The HTML Pre formatted by HTML5 Tab Player
If the user feels that the tab takes too much space on the web page, he/she can collapse the Tab Player, so that only the toolbar is visible. If you are displaying many tabs at the same time, this can avoid unnecessary cluttered pages:
Figure 8. The HTML5 Tab Player in collapsed mode
On the ther hand, sometimes the user may want to see the whole tablature, no matter how large it is. The expandbutton removes the <pre> tag height limit, and so it is shown in its entirety:
Figure 9. The HTML5 Tab Player in full size mode
When the page is ready, the tab is immediately formatted and the user can access the tab player controls. The small blue circles around the numbers in the image below represent the notes being played.
The Tuning setting defines the "open" string notes (guitars usually have 6 strings, and an "open" string mean that you are picking a string with one hand without pressing any fret with the other hand). Most of the songs will be played with the standard "EADGBE" tuning, but you can change the tuning if the song requires you to do so.
Figure 10. The plug-in interprets and plays the tablature
The Bar Duration setting defines how long should it take to play each of the tablature bars. The bars are the segments between the column of pipes, as shown in the image below.
Figure 11. The player displaying bar duration options

The code

The plugin initializes when the page is completely loaded. The setupTabPlayer method is applied to each and everypre tag containing the tabplayer as the value for the language attribute.

$(function () {
    $('pre[lang=tabplayer]').setupTabPlayer();
});

Figure 12. Plugin initialization
Inside the plugin initialization, we keep the notesarray for the 12 semitones, which are the same 7 white keys plus the 5 keys on the piano. These notes are repeated four times in the octaves, as to represent the four octaves used in the plugin.

    var octaves = [];
    var notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];

Figure 13. The 12 notes
Along with the octaves initialization, we create one audio HTML tag for each note. The audio tag is provided with an id and source file, and is configured for preloading (this ensures that we don't have delays while playing for the first time).
The sounds were obtained from the package uploaded by member Kyster to the Freesound.org website, under the Creative Commons license. Notice that each note has an independent and corresponding ogg file. All notes were recorded from nylon strings played in acoustic guitar, providing a deep and natural sound to our tab player.
Finally, each audio tag is appended to the body of the page, where it remains invisible and in stand-by mode.

    function initializeOctaves() {
        for (var i = 1; i <= 4; i++) {
            $(notes).each(function (index, note) {
                var noteName = i + note.replace('#', 'sharp');
                octaves.push(noteName);
                var audio = $('<audio>')
                            .attr({
                                id: 'note' + noteName,
                                src: 'tabplayer/sounds/' + noteName + '.ogg',
                                preload: 'auto'
                            });
                $('body').append(audio);
            });
        }
    }

Figure 14. Creating one audio HTML tag for each note
Next, we have the initialization variables that are referenced throughout the code. Notice that, because a single web page can hold any number of tab players, we must ensure that each one has its own set of control variables (tuning, volume, playing status, etc.) so as to not interfere with each other players.
  • tabPlayerId: the ID for the player
  • step: the horizontal position (i.e. the cursor) of the tab as the player search for notes
  • guitarStrings: contains information about the strings
  • interval: the interval object created by setInterval function
  • isPaused: indicates if the player is paused or not
  • volume: not used (for future releases)
  • barTime: indicates how long it takes to complete each bar in the tablature
  • isShowingTuning: indicates if the current configuration is tuning or bar time


        initializePlayerVars: function() {
            var me = this;
            me.tabPlayerId =  undefined;
            me.step = 0;
            me.noteSeq = 0;
            me.guitarStrings = [{
                openString: 'e',
                currentNoteIndex: 0
            }, {
                openString: 'B',
                currentNoteIndex: 0
            }, {
                openString: 'G',
                currentNoteIndex: 0
            }, {
                openString: 'D',
                currentNoteIndex: 0
            }, {
                openString: 'A',
                currentNoteIndex: 0
            }, {
                openString: 'E',
                currentNoteIndex: 0
            }],
            me.interval = undefined;
            me.isPaused = false;
            me.volume = 1;
            me.barTime = 3000;
            me.isShowingTuning = true;
        },

Figure 15. The plugin variables held for each tab in the page
The octaveNoteIndex property indicates the position of each string inside the octaves array. People acquainted with guitar will notice in the intervals that this follows the standard guitar tuning: 5th fret / 5th fret / 5th fret / 4th fret / 5th fret.

        initializeGuitarStrings: function () {
            var me = this;
            me.guitarStrings[0].octaveNoteIndex = 28;
            me.guitarStrings[1].octaveNoteIndex = 23;
            me.guitarStrings[2].octaveNoteIndex = 19;
            me.guitarStrings[3].octaveNoteIndex = 14;
            me.guitarStrings[4].octaveNoteIndex = 9;
            me.guitarStrings[5].octaveNoteIndex = 4;
        },

Figure 16. Configuring the player for standard guitar tuning

        initializeAudioChannels: function () {
   var me = this;
            for (a = 0; a < me.channel_max; a++) {         // prepare the channels
                this.audiochannels[a] = new Array();
                this.audiochannels[a]['channel'] = new Audio();      // create a new audio object
                this.audiochannels[a]['finished'] = -1;       // expected end time for this channel
            }
        },

Figure 17. Initializing audiochannels
The control panel features the buttons necessary to control the player process: play, pause, stop, along with buttons that control the height (minimize, full size and window). Also, the tuning and the bar time drop down lists.
All these HTML elements are created on the fly using jQuery functions.

        createControlPanel: function () {
            var me = this;
            var aPlay = $('<a>')
                .addClass('playerButton')
                .addClass('play')
                .click(function () {
                    me.play();
                });
            var imgPlay = $('<img>');

            $(aPlay).append(imgPlay);

            var aPause = $('<a>')
            .addClass('disabled')
            .addClass('playerButton')
            .addClass('pause')
            .click(function () {
                me.pause();
            });
            $(aPause).append($('<img>'));
            
            var aStop = $('<a>')
             .addClass('disabled')
            .addClass('playerButton')
            .addClass('stop')
            .click(function () {
                me.stop();
            });
            $(aStop).append($('<img>'));

            var aSettings = $('<a>')
            .addClass('playerButton')
            .addClass('settings')
            .click(function () {
                me.settings();
            });
            $(aSettings).append($('<img>'));

            var aPreMinimize = $('<a>')
            .addClass('playerButton')
            .addClass('minimize')
            .click(function () {
                me.resizeToMinimize();
            });
            $(aPreMinimize).append($('<img>'));

            var aPreWindow = $('<a>')
            .addClass('playerButton')
            .addClass('window')
            .click(function () {
                me.resizeToWindow();
            });
            $(aPreWindow).append($('<img>'));

            var aPreMaximize = $('<a>')
            .addClass('playerButton')
            .addClass('maximize')
            .click(function () {
                me.resizeToMaximize();
            });
            $(aPreMaximize).append($('<img>'));

            var lblTempo = $('<span>').append('Bar duration (ms)');

            var ddlTempo = $('<select>').addClass('ddlTempo');
            for (var i = 500; i < 5000; i += 100) {
                var option = $('<option>').val(i).html(i);
                $(ddlTempo).append($(option));
            }

            var lblTuning = $('<span>').append('Tuning');

            var ddlTuning1 = $('<select>').addClass('ddlTuning1');
            $(ddlTuning1).append('<option note="C" value="0">C</option>');
            $(ddlTuning1).append('<option note="C#" value="1">C#</option>');
            $(ddlTuning1).append('<option note="D" value="2">D</option>');
            $(ddlTuning1).append('<option note="D#" value="3">D#</option>');
            $(ddlTuning1).append('<option note="E" value="4" selected>E</option>');
            var ddlTuning2 = $('<select>').addClass('ddlTuning2');
            $(ddlTuning2).append('<option note="F" value="5">F</option>');
            $(ddlTuning2).append('<option note="F#" value="6">F#</option>');
            $(ddlTuning2).append('<option note="G" value="7">G</option>');
            $(ddlTuning2).append('<option note="G#" value="8">G#</option>');
            $(ddlTuning2).append('<option note="A" value="9" selected>A</option>');
            var ddlTuning3 = $('<select>').addClass('ddlTuning3');
            $(ddlTuning3).append('<option note="A#" value="10">A#</option>');
            $(ddlTuning3).append('<option note="B" value="11">B</option>');
            $(ddlTuning3).append('<option note="C" value="12">C</option>');
            $(ddlTuning3).append('<option note="C#" value="13">C#</option>');
            $(ddlTuning3).append('<option note="D" value="14" selected>D</option>');
            var ddlTuning4 = $('<select>').addClass('ddlTuning4');
            $(ddlTuning4).append('<option note="D#" value="15">D#</option>');
            $(ddlTuning4).append('<option note="E" value="16">E</option>');
            $(ddlTuning4).append('<option note="F#" value="17">F</option>');
            $(ddlTuning4).append('<option note="F" value="18">F#</option>');
            $(ddlTuning4).append('<option note="G" value="19" selected>G</option>');
            $(ddlTuning4).append('<option note="G#" value="20">G#</option>');
            $(ddlTuning4).append('<option note="A" value="21">A</option>');
            var ddlTuning5 = $('<select>').addClass('ddlTuning5');
            $(ddlTuning5).append('<option note="G" value="19">G</option>');
            $(ddlTuning5).append('<option note="G#" value="20">G#</option>');
            $(ddlTuning5).append('<option note="A" value="21">A</option>');
            $(ddlTuning5).append('<option note="A#" value="22">A#</option>');
            $(ddlTuning5).append('<option note="B" value="23" selected>B</option>');
            $(ddlTuning5).append('<option note="C" value="24">C</option>');
            $(ddlTuning5).append('<option note="C#" value="25">C#</option>');
            $(ddlTuning5).append('<option note="D" value="26">D</option>');
            var ddlTuning6 = $('<select>').addClass('ddlTuning6');
            $(ddlTuning6).append('<option note="C" value="24">C</option>');
            $(ddlTuning6).append('<option note="C#" value="25">C#</option>');
            $(ddlTuning6).append('<option note="D" value="26">D</option>');
            $(ddlTuning6).append('<option note="D#" value="27">D#</option>');
            $(ddlTuning6).append('<option note="E" value="28" selected>E</option>');

            var divTabPlayerControls = $('<div>').addClass('tabPlayerControls').attr('tabPlayerId', me.tabPlayerId);
            $(me.el).attr('tabPlayerId', me.tabPlayerId);

            $(divTabPlayerControls).append($(aPlay));
            $(divTabPlayerControls).append($(aPause));
            $(divTabPlayerControls).append($(aStop));
            $(divTabPlayerControls).append($(aSettings));

            $(divTabPlayerControls).append($(aPreMaximize));
            $(divTabPlayerControls).append($(aPreWindow));
            $(divTabPlayerControls).append($(aPreMinimize));

            var divTempo = $('<div>').hide().addClass('barTime');
            $(divTempo).append($(lblTempo));
            $(divTempo).append($(ddlTempo));
            $(divTabPlayerControls).append($(divTempo));

            var divTuning = $('<div>').addClass('tuning');
            $(divTuning).append($(lblTuning));
            $(divTuning).append($(ddlTuning1));
            $(divTuning).append($(ddlTuning2));
            $(divTuning).append($(ddlTuning3));
            $(divTuning).append($(ddlTuning4));
            $(divTuning).append($(ddlTuning5));
            $(divTuning).append($(ddlTuning6));
            $(divTabPlayerControls).append($(divTuning));

            $(me.el).before($(divTabPlayerControls));
        },

Figure 18. Adding HTML elements to the toolbar panel at run time
It is possible to predefine the bar time variable by just adding the barTime attribute in the pre tag:

        setupBarTime: function() {
            var me = this;
            var barTime = $(me.el).attr('barTime');
            if (barTime) {
                me.barTime = barTime;
                var option = $('div.tabPlayerControls[tabPlayerId=' + me.tabPlayerId + '] select.ddlTempo option[value=' + barTime + ']');
                if (option.length > 0) {
                    $(option).prop('selected', true);
                }
            }
        },

Figure 19. The setupBarTime function configures the bar time via attribute
It is possible to predefine the tuning variable by just adding the tuning attribute in the pre tag:

        setupTuning: function () {
            var me = this;
            var tuning = $(me.el).attr('tuning');
            if (tuning) {
                var note = '';
                var stringIndex = 0;
                for (var i = tuning.length - 1; i >= 0; i--) {
                    note = tuning[i] + note;
                    var option = $('div.tabPlayerControls[tabPlayerId=' + me.tabPlayerId + '] select.ddlTuning' + (6 - stringIndex) + ' option[note=' + note + ']');
                    if (option.length > 0) {
                        $(option).prop('selected', true);
                        note = '';
                        stringIndex++;
                    }
                }
            }
        },

Figure 20. The setupTuning function configures the tuning via attribute
The formatPre function performs an important task in our application: it replaces the note numbers inside the tab with span tags that highlight the notes.

        formatPre: function () {
            var me = this;
            var lines = $(me.el).html().split('\n');
            var html = ''

            var lineIndex = 0;
            var tabStripLine = -1;
            $(lines).each(function (index, line) {
                if (line.indexOf('-') >= 0 && line.indexOf('|') >= 0) {
                    var isNewTabStrip = false;
                    if (tabStripLine == -1) {
                        tabStripLine = index;
                        isNewTabStrip = true;
                    }

                    html += line.replace(/(\d+)/gm, function (expression, n1, n2) {
                        var noteName = 'note' + octaves[parseInt(me.guitarStrings[lineIndex].octaveNoteIndex) + parseInt(expression)];
                        return '<span class="note" title="' + noteName.replace(/\d/, '').replace('note', '') + '" string="' + lineIndex + '" pos="' + n2 + '" tabStripLine="' + tabStripLine + '">' + expression + '</span>';
                    }) + '\n';
                    lineIndex++;
                    if (lineIndex == 6)
                        lineIndex = 0;
                }
                else {
                    lineIndex = 0;
                    html += line + '\n';
                    tabStripLine = -1;
                }
            });
            $(me.el).html(html);

            var noteId = 1;
            $($(me.el).find('span.note')).each(function (index, span) {
                $(span).attr({ noteId: noteId++});
            });
        },

Figure 22. The formatPre function highlights the notes
The play_multi_sound function searches for finished channels and use them to play one specific sound.

        play_multi_sound: function (s, stringIndex, volume) {
            var me = this;
            for (a = 0; a < me.audiochannels.length; a++) {
                var volume = me.audiochannels[a]['channel'].volume;
                me.audiochannels[a]['channel'].volume = (volume - .2) > 1 ? volume - .2 : volume;
            }

            for (a = 0; a < me.audiochannels.length; a++) {
                thistime = new Date();
                if (me.audiochannels[a]['finished'] < thistime.getTime()) {   // is this channel finished?
                    if (document.getElementById(s)) {
                        me.audiochannels[a]['finished'] = thistime.getTime() + document.getElementById(s).duration * 1000;
                        me.audiochannels[a]['channel'].src = document.getElementById(s).src;
                        me.audiochannels[a]['channel'].load();
                        me.audiochannels[a]['channel'].volume = [0.4, 0.5, 0.6, 0.7, 0.9, 1.0][stringIndex];
                        me.audiochannels[a]['channel'].play();
                        
                        break;
                    }
                }
            }
        },

Figure 23. The play_multi_sound function
The play function is divided in some internal functions, that parse, organize, calculate which sounds correspond to each note value, and - obviously - tell the audio files to be played accordingly.
The checkStep function finds out how many characters exist in a single bar, and then calculates the interval time allocated for each step in the process.
The playStep function calculates the corresponding audio file and plays it.
The configureInterval controls the player's "heartbeat" and provides the tempo for the music.

        play: function () {
            var me = this;
            me.enablePauseButton();
            if (me.isPaused) {
                me.isPaused = false;
                return;
            }
            $(me.el).find('span.note').removeClass('played');
            var ddlTempo = $('div.tabPlayerControls[tabPlayerId=' + me.tabPlayerId + '] select.ddlTempo');
            me.barTime = $(ddlTempo).val();

            $(me.guitarStrings).each(function (stringIndex, guitarString) {
                var ddlTuning = $('div[tabPlayerId=' + me.tabPlayerId + '] .ddlTuning' + (6 - stringIndex));
                me.guitarStrings[stringIndex].octaveNoteIndex = ddlTuning.val();
                me.guitarStrings[stringIndex].currentNoteIndex = 0;
            });

            var pre = $(me.el);
            var lines = $(pre).html().split('\n');
            var tabLines = ['', '', '', '', '', ''];
            var lineIndex = 0;
            $(lines).each(function (index, line) {
                if (line.indexOf('-') >= 0 && line.indexOf('|') >= 0) {
                    tabLines[lineIndex] = tabLines[lineIndex] + line.trim().substring(1).replace(/(<([^>]+)>)/ig, "");;
                    lineIndex++;
                    if (lineIndex == 6)
                        lineIndex = 0;
                }
                else {
                    lineIndex = 0;
                }
            });

            var stepCount = tabLines[0].trim().length

            var checkStep = function () {
                $(tabLines).each(function (index, tabLine) {
                    tabLine = tabLine.trim();
                    var fretValue = tabLine[step];
                    if (index == 0 && (fretValue == '|' || ('EADGBe'.indexOf(fretValue) >= 0))) {

                        var sub = tabLine.substring(step + 3);
                        var barLength = sub.indexOf('|');
                        if (barLength > 0) {
                            step += 1;
                            configureInterval(barLength);
                        }
                    }
                });
            }

            var playStep = function () {
                var stepCharLength = 1;
                var stepHasDoubleDigitNote = false;
                $(tabLines).each(function (index, tabLine) {
                    tabLine = tabLine.trim();
                    if (!isNaN(tabLine[step]) && !isNaN(tabLine[step + 1])) {
                        stepHasDoubleDigitNote = true;
                        return false;
                    }
                });

                $(tabLines).each(function (index, tabLine) {
                    tabLine = tabLine.trim();

                    var guitarString = me.guitarStrings[index];
                    var fretValue = '';
                    if (stepHasDoubleDigitNote) {
                        fretValue = (tabLine[step] + '' + tabLine[step + 1]).replace('-', '');
                        stepCharLength = 2;
                    }
                    else {
                        fretValue = tabLine[step];
                    }


                    if (!isNaN(fretValue)) {
                        var span = $(me.el).find('span.note[string=' + index + ']:eq(' + me.guitarStrings[index].currentNoteIndex + ')');
                        $(span).addClass('played').addClass(fretValue.length == 1 ? 'onedigit' : 'twodigits');
                        fretValue = parseInt(span.html());
                        me.guitarStrings[index].currentNoteIndex++;

                        var noteName = 'note' + octaves[parseInt(guitarString.octaveNoteIndex) + parseInt(fretValue)];

                        me.play_multi_sound(noteName, index, me.volume);
      
                        me.volume = .5;
                
                        me.noteSeq++;      
                    }
                });
                return stepCharLength;
            }

            var configureInterval = function (newBarLength) {
                if (me.interval)
                    clearInterval(me.interval);

                me.interval = setInterval(function () {
                    if (!me.isPaused) {
                        checkStep();
                        var stepCharLength = playStep();
                        step += stepCharLength;
                        if (step >= stepCount) {
                            clearInterval(me.interval);
                            me.enablePlayButton();
                        }
                    }
                }, me.barTime / me.BAR_LENGTH);
            }

            var step = 0;

            configureInterval(me.BAR_LENGTH);
        },

Figure 24. The play function, the most important piece of code

Conclusion

Figure 12. The HTML5 Tab Player - Classical Demo Page
As you can see, there is so much room for improvement... Also, other music-related components may be built on top of the plug-in (in fact, I wish to publish a tablature composer that I'm thinking of very soon). I hope you enjoyed the HTML5 Tab Player plug-in! Please leave your complaint or support in the comment section below. Let me know what you are thinking.

Source from
http://www.codeproject.com/Articles/834206/HTML-Guitar-Tab-Player

No comments :

Post a Comment