65.9K
CodeProject 正在变化。 阅读更多。
Home

HTML5 吉他谱播放器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (71投票s)

2014年10月28日

CPOL

8分钟阅读

viewsIcon

88996

downloadIcon

1728

将纯文本吉他谱转换为在线可播放音乐的 jQuery 插件。

Html5 Tab Player

图 1. HTML5 谱播放器 - Beatles 演示页面

引言

如果您弹过吉他,您可能偶尔使用过六线谱(也称为“谱”)。如果您曾在网上搜索过吉他谱,大多数时候您都会发现是同一种纯文本格式的六线谱。

有些网站提供谱播放器,但在大多数情况下,您需要先注册一个付费的高级账户才能使用该服务。而且在大多数情况下,这项服务将运行在 Flash 中。

但如果您能立即播放那些纯文本吉他谱呢?如果您喜欢这个主意,那么这篇文章就是为您准备的。HTML5 谱播放器 是一个 jQuery 插件,它允许您轻松地将吉他谱嵌入到您的网页中,而无需依赖 Flash 插件。

背景

对我来说,将音乐和编程结合在一篇文章中是一个梦想成真。编程是我谋生的手段,但最近我一直在周末花费更多宝贵的时间来弹奏原声吉他。

图 2. 嘿,那是 Bob!

当我为特定的歌曲在网上寻找吉他谱时,HTML5 谱播放器 jQuery 插件的想法 came to me,然后我突然意识到它们都是纯文本。接下来我想到,大多数编程博客和面向程序员的网站,例如Code Project,都使用格式化组件,这些组件会解析并将贡献者提交的纯文本代码片段转换为更易读、格式良好的 HTML 内容。也就是说,格式化会考虑所呈现的编程语言的语义,然后该语言的所有关键字都会被相应地突出显示或着色,而无需人工干预。

系统依赖

HTML5 谱播放器插件完全在客户端运行,只需部署配套文件即可运行。

  • tabplayer.css:您可以在其中自定义 HTML5 谱播放器外观的样式表。
  • img 文件夹:包含 HTML5 谱播放器工具栏的图像。
  • jQuery-1.9.1.min.js:插件代码构建在其上的 jQuery。
  • tabplayer.js:包含所有插件功能。
  • sounds 文件夹:包含所有声音文件,每个文件代表一个独特的原声吉他音符。

 

图 3. 文件依赖

插件做什么(以及不做什么)

如前所述,当我观察到像 codeproject.com 这样的网站将纯代码格式化为格式良好、高亮显示的 HTML 内容时,我脑海中就有了这个插件的想法。例如,看看Prettify

图 4. Prettify:输入纯代码,输出漂亮代码

如果您认为互联网上找到的大多数吉他谱只是纯文本,并且它们包含在 <pre> 标签中,那么它们可以被视为等待格式化的代码。下面的示例摘自ultimate-guitar.com

图 5. Ultimate Guitar 谱网站总是在 <pre> 标签内显示谱

作为输入,我们的 HTML5 谱播放器接受 <pre> 标签中的以下六线谱。请注意,它需要 lang=tabPlayer 属性。其他属性,例如 bartimeclass 将稍后解释。

图 6. 未格式化的 HTML Pre 标签

结果是,HTML5 谱播放器格式化了纯文本六线谱,高亮显示音符数字,并在谱之前创建了一个工具栏面板,允许用户播放、暂停、停止和配置播放器。

图 7. 由 HTML5 谱播放器格式化的 HTML Pre

如果用户觉得谱在网页上占用了太多空间,他们可以折叠谱播放器,以便只显示工具栏。如果您同时显示多个谱,这可以避免不必要的杂乱页面。

图 8. HTML5 谱播放器折叠模式

另一方面,有时用户可能希望看到整个六线谱,无论它有多大。expand 按钮消除了 <pre> 标签的高度限制,因此它会完整显示。

图 9. HTML5 谱播放器全屏模式

页面准备就绪后,谱会立即格式化,用户可以访问谱播放器控件。下图中的数字周围的小蓝色圆圈代表正在播放的音符。

Tuning 设置定义了“空弦”(吉他通常有 6 根弦,而“空弦”意味着用一只手弹奏一根弦,而另一只手不按任何品格)的音符。大多数歌曲将使用标准的“EADGBE”调音,但如果歌曲要求,您可以更改调音。

图 10. 插件解析并播放六线谱

Bar Duration 设置定义了播放谱中每个小节所需的时间。小节是符号列之间的段落,如下图所示。

图 11. 播放器显示小节时长选项

代码

当页面完全加载后,插件会初始化。setupTabPlayer 方法应用于包含 tabplayer 作为语言属性值的每个 pre 标签。

 

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

 

图 12. 插件初始化

在插件初始化内部,我们保留了 12 个半音的 notes 数组,它们与钢琴上的 7 个白键加上 5 个黑键相同。这些音符在 octaves 中重复四次,以表示插件中使用的四个八度。

 

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

 

图 13. 12 个音符

与八度初始化一起,我们为每个音符创建了一个 audio HTML 标签。audio 标签提供了 ID 和源文件,并配置为预加载(这可以确保我们第一次播放时没有延迟)。

声音来自成员 Kyster 上传到 Freesound.org 网站的软件包,根据知识共享许可。请注意,每个音符都有一个独立对应的 ogg 文件。所有音符均从原声吉他的尼龙弦录制,为我们的谱播放器提供深沉自然的音色。

最后,每个 audio 标签都被附加到页面主体,在那里它们保持不可见并处于待机模式。

 

    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);
            });
        }
    }

 

图 14. 为每个音符创建一个 audio HTML 标签

接下来,我们有了在代码中引用的初始化变量。请注意,由于一个网页可以包含任意数量的谱播放器,我们必须确保每个播放器都有自己的一组控制变量(调音、音量、播放状态等),以免与其他播放器相互干扰。

  • tabPlayerId:播放器的 ID
  • step:当播放器搜索音符时,谱的水平位置(即光标)。
  • guitarStrings:包含有关琴弦的信息。
  • interval:由 setInterval 函数创建的间隔对象。
  • isPaused:指示播放器是否暂停。
  • volume:未使用(用于未来版本)。
  • barTime:指示完成六线谱中每个小节所需的时间。
  • isShowingTuning:指示当前配置是调音还是小节时长。

 

 

        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;
        },

 

图 15. 页面中每个谱的插件变量

octaveNoteIndex 属性指示每个琴弦在 octaves 数组中的位置。熟悉吉他的人会注意到,这里的间隔遵循标准的吉他调音:5 品 / 5 品 / 5 品 / 4 品 / 5 品。

 

        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;
        },

 

图 16. 配置播放器以进行标准吉他调音

 

        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
            }
        },

 

图 17. 初始化音频通道

控制面板提供了控制播放器过程的必要按钮:playpausestop,以及控制高度的按钮(minimizefull sizewindow)。此外,还有调音和小节时长下拉列表。

所有这些 HTML 元素都是使用 jQuery 函数即时创建的。

 

        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));
        },

 

图 18. 在运行时将 HTML 元素添加到工具栏面板

通过在 pre 标签中添加 barTime 属性,可以预定义 bar time 变量。

 

        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);
                }
            }
        },

 

图 19. setupBarTime 函数通过属性配置小节时长

通过在 pre 标签中添加 tuning 属性,可以预定义 tuning 变量。

 

        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++;
                    }
                }
            }
        },

 

图 20. setupTuning 函数通过属性配置调音

formatPre 函数在我们的应用程序中执行一项重要任务:它将谱中的音符数字替换为高亮显示音符的 span 标签。

 

        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++});
            });
        },

 

图 22. formatPre 函数高亮显示音符

play_multi_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;
                    }
                }
            }
        },

 

图 23. play_multi_sound 函数

play 函数分为几个内部函数,用于解析、组织、计算哪些声音对应于每个音符值,以及——当然——指示音频文件相应地播放。

checkStep 函数找出单个小节中有多少个字符,然后计算在此过程中为每个步骤分配的间隔时间。

playStep 函数计算对应的音频文件并播放它。

configureInterval 控制播放器的“心跳”并为音乐提供节奏。

 

        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);
        },

 

图 24. play 函数,最重要的代码部分

结论

图 12. HTML5 谱播放器 - Classical 演示页面

如您所见,还有很多改进的空间……此外,还可以基于此插件构建其他与音乐相关的组件(事实上,我希望很快能发布一个我正在考虑的六线谱编辑器)。希望您喜欢 HTML5 谱播放器插件!请在下面的评论区留下您的意见或支持。告诉我您的想法。

历史

2014-10-28:第一个版本。

© . All rights reserved.