HTML5 吉他谱播放器






4.99/5 (71投票s)
将纯文本吉他谱转换为在线可播放音乐的 jQuery 插件。
图 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 属性。其他属性,例如 bartime 和 class 将稍后解释。
图 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. 初始化音频通道
控制面板提供了控制播放器过程的必要按钮:play、pause、stop,以及控制高度的按钮(minimize、full size 和 window)。此外,还有调音和小节时长下拉列表。
所有这些 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:第一个版本。