我今天要和你们分享的是一个我本身写的音乐网页小程序,这个网页程序主要分为两个部分--即时演奏(LivePlay)和编曲(Arranger)。即时演奏就是指按下鼠标/键盘/手机屏幕就能够即刻发声,编曲是指提早写好“谱子”而后播放。javascript
这个音乐程序如今仅有网页版,因为我使用Javascript(和HTML,CSS)写成,因此理论上未来它能够移植到Android和iOS上,也能够改为电脑程序,固然也能够改装成微信小程序!html
我是一个Javascript和Web初学者,这个音乐小程序并不复杂,因此若是有喜欢音乐,或在学习Web前端,学习canvas绘图的朋友,你们能够一块儿探讨程序的机理,体悟美妙的音乐!前端
网页示范: https://sien75.github.io/MusicMaker/liveplay , 在浏览器中打开就能够啦(ie,edge除外,手机记得横屏)html5
个人github主页就是 https://github.com/sien75,看完整代码来这里就能够,欢迎加星,不胜感激^ >< ^java
最初,学校的C++课程有写程序的课题任务,我萌生了作一个编曲&即时演奏的音乐程序的念头,因而我在网上不断查找,找到了MIDI(Musical Instrument Digital Interface,乐器的数字接口)这玩意。使用Windows的MIDI消息api--midiOutShortMSG(...),能够发送MIDI消息,而后Windows利用自带的MIDI音色库生成声音。我花费了一个月的时间,用MFC实现了一个简陋的音乐程序。以后,我想进一步把这个程序写下去,使程序更完善,可是我发现本身写的烂代码本身根本不肯回顾……c++
并且MFC是一个比较老的东西了,因此我想丢掉以前的代码,从新写一个程序(话说在我不停地“备份-格式化磁盘-换系统”中,那份原始代码终于被我删掉了……)。我想我不是已经会c++了嘛,因此我最初尝试用Qt写。然而我发现Qt没有关于MIDI的api,我也在网上搜索了好一阵子,也没有找到合适的第三方库,因而就不了了之了。git
还有我想实现跨平台的程序,既然Qt & C++不能用了,我想继续用C#写下去。缘由以下:1 C#看起来和C++挺像的,应该容易学习;2 VisualStudio + C#号称天下无敌宇宙第一,且跨平台很轻松;3 C#也可使用Windows的MIDI api,我不用再愁发不出声的问题了;4 看看“C#”这名字,命名人确定很喜欢音乐,这个语言写音乐程序确定很适合。github
然而以后再次放弃,具体缘由忘记了,多是我一直想学习Web安全领域,因此我火烧眉毛要开始前端之路了。因而花费了一些时间学习HTML,学习CSS,学习Javascript(强烈推荐《Javascript高级程序设计》)。web
据说w3c有个Web MIDI Api,我想:何不用这个东西实现音乐程序呢?并且这个浏览器自己就是跨平台的,这样正好符合个人要求。然而Web MIDI Api是为了在浏览器上使用MIDI硬件设备的,并不能直接解决个人问题。与是我又花了很长时间,不停地找,无数次想放弃,可是最终,我找到了一个perfect的东西(大神的东西……) https://github.com/surikov/webaudiofont。canvas
这不是MIDI,MIDI发声原理是主控器(好比MIDI键盘)发送信号,经音序器(Sequencer)处理,使内置音乐播放器调用音源,进而使扬声器发声。因此MIDI传输的是数字符号,用来表示音乐的起伏。这个库就是模仿的这一过程,咱们能够经过键盘鼠标手机触摸屏(至关于主控器)进行编辑,而后经过html5的Web Audio Api(至关于Windows的内置音乐播放器)播放音源发声,这里的音源文件,那位大神也已经准备好了,https://github.com/surikov/webaudiofontdata,这里面有一百多种乐器的音源(即MIDI的那些标准乐器,好比钢琴吉他贝斯尺八)。而这个库就是一个Javascript版的音序器,它已经能够实现发出不一样声调不一样音色的声音的功能。
因而,我就开始写代码,以后的事情有章可循,比以前的迷茫要好一些了。
接下来我就说一下这个程序的具体代码,阅读前确保您已掌握HTML,CSS,Javascript,HTML5 canvas绘图和一些音乐基本知识。
程序分为两个部分,即时演奏(LivePlay)和编曲(Arranger),目前只实现了LivePlay模块,Arranger正在码代码中。来看一下LivePlay模块的使用,放图片:
如图,界面中心是五个键组,每一个键组有7个白键,因此一共有35个白键,分别表明音调C2 D2 E2 F2 G2 A2 B2 C3 D3 ... A6 B6。其中C4~B4便是一般所说的do re mi fa so la si啦。除了白键,还有25个黑键,这些就是相应的半调C# D# F# G# A#了。用鼠标点击黑白键,或点击后拖动,皆可发出声音。用键盘控制方法以下:
按下对应的键,就能够发声。K键或左方向键能够向左切换键组,同理L键或右方向键能够向右切换键组。键组从小字一组切换到小字二组的示意图以下:
切换键组后键盘上相应的12个键就能够控制当前键组的12个音调了。
因为手机没有键盘,因此不存在切换键组的问题,可是使用的时候记得横屏。
界面上部有4个下拉框,分别能够改变音色,八度升降,键盘控制的键组和键组数目,这些改变是显而易见的,你们本身试一试吧。
最后,右上角的swith to arranger能够跳转到本程序的编曲(Arranger)部分(正在施工中)。
这是本程序的代码根目录,其中arranger和liveplay即为程序的两个主要模块,sound存放音源,browser.js用于检测客户端类型(主要看看是否是在用手机浏览本站),index.html是程序主页(固然这个主页如今没什么用,会自动跳转到liveplay/index.html),webaudiofontplayer.js是js音序器。
在liveplay里,有7个文件:
首先,index.html是网页入口。main.js的功能是定义页面整体设置函数和初始化函数,三个“eventhandler”文件是处理事件(好比下拉框的选项选择啦,键盘按下啦……)的,而后myAudio.js和myCanvas.js分别定义了MyAudio()和MyCanvas()两个构造函数,分别用于处理声音和绘图部分。网页运行流程以下:
刚打开时会运行main.js中的init()函数,该函数进行整体设置的初始化,并分别调用myAudio.js和myCanvas.js中的初始化函数进行声音部分和绘图部分的初始化,初始化完毕后,程序等待用户事件的发生。若是用户在电脑端按下键盘或用鼠标点击琴键,会触发PCEventHandlers.js中的响应函数;若是用户在手机端触摸琴键区,会触发mobileEventHandlers.js中的响应函数;若是用户操做下拉框,会触发eventHandlers.js中的响应函数。全部响应函数会实际上调用main.js,myAudio.js或myCanvas.js中的函数进行具体的操做,以完成所需效果。
你们想,这个程序显示上最重要的就是canvas区域,而声音不须要显示区域,因此,index.html文件仍是很是简短的。在index.html中,主要的就有四个select标签控制音色,八度,键盘所控键组和键组数目,和一个canvas标签。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, 6 maximum-scale=1.0, user-scalable=no"> 7 <title>LivePlay 即时演奏</title> 8 </head> 9 <body style="user-select:none; margin:0; overflow:hidden; background:#222; 10 font-family:'Lucida Console',Monaco,monospace"> 11 <div style="margin-bottom:5px"> 12 <h1 style="font-size:25px; color:#888; display:inline-block; margin:4px 0 0 5px; 13 border:3px solid; border-radius:5px">MusicMaker</h1> 14 <h1 style="font-size:28px; display:inline-block; color:#888; margin:6px 20px 0 0;">LivePlay</h1> 15 <select id="selectInstruments"> 16 <option value="0000_Aspirin:n">piano</option> 17 <option value="0390_Aspirin:y">bass</option> 18 </select> 19 <select id="octive"> 20 <option value="0" id="o0">八度:0</option> 21 <option value="1" id="o1">八度:+1</option> 22 <option value="-1" id="on1">八度:-1</option> 23 <option value="2" id="o2">八度:+2</option> 24 <option value="-2" id="on2">八度:-2</option> 25 </select> 26 <select id="keyboardGroup"> 27 <option value="0" id="k1">键盘控制小字一组</option> 28 <option value="-2" id="k4">键盘控制大字组</option> 29 <option value="-1" id="k2">键盘控制小字组</option> 30 <option value="1" id="k3">键盘控制小字二组</option> 31 <option value="2" id="k5">键盘控制小字三组</option> 32 </select> 33 <select id="groupNum"> 34 <option value="5" id="g5">键组数目:5</option> 35 <option value="4" id="g4">键组数目:4</option> 36 <option value="3" id="g3">键组数目:3</option> 37 <option value="2" id="g2">键组数目:2</option> 38 <option value="1" id="g1">键组数目:1</option> 39 </select> 40 <span id="loading" style="font-size:20px; color:#f22; margin-left:10px; display:none ">Loading...</span> 41 <a href="../arranger/index.html" style="font-size:15px; color:#888; 42 float:right; margin-top:20px" id="switch">switch to arranger</a> 43 </div> 44 <canvas id="canvas"></canvas> 45 <script type="text/javascript" src="../browser.js"></script> 46 <script type="text/javascript" src="../webAudiofontPlayer.js"></script> 47 <script type="text/javascript" src="myCanvas.js"></script> 48 <script type="text/javascript" src="myAudio.js"></script> 49 <script type="text/javascript" src="main.js"></script> 50 <script type="text/javascript" src="eventHandlers.js"></script> 51 <script type="text/javascript" src="PCEventHandlers.js"></script> 52 <script type="text/javascript" src="mobileEventHandlers.js"></script> 53 </body> 54 </html>
index.html很是简单,第5,6行是禁止手机浏览器双击放大和双指放大的。
第9行的user-select:none,是禁止鼠标选取内容的,本程序使用过程当中会拖动鼠标,因此咱们必须禁止默认的拖动选中。
第44行就是一个canvas画布,咱们会在js里对其进行设置,咱们接下来的很大一部分工做就是针对这个画布的。
接下来咱们就来分析js文件。
main.js
刚才说过,main.js有两个部分,初始化函数的定义和整体设置函数的定义。图示,这两部分,分别有3个函数:
这是一个初始化的大致的流程图,红色箭头表明初始化流程进行路线,黑色箭头表明初始化函数的调用状况。
handleOctive()和handleKeyboardGroup()两个函数要按照当时的键组数目进行调整,先讨论handleOctive()。
咱们一共有60个键,分别对应音值24~83这60个音调。若是调整键组数目为4个,那么就会有12*4 = 48个键,它们对应24~71这48个音调,因此这时能够升八度,使其对应于36~83这48个音调。若是调整键组数目为3个,那么就会一共有12*3 = 36个键,初始对应于36~71这36个音调,因此既能够升八度到48~83,也能够降八度到24~59。
那么,键组数目与能够升降八度的状况有以下对应:
5 ~ 无; 4 ~ (+1); 3 ~ (-1, +1); 2 ~ (+2, -1, +1); 1 ~ (-2, +2, -1, +1)
因此咱们定义以下数组:
var octs = ['n2', '2', 'n1', '1'];
实现按照顺序隐藏或显示相应的八度调整选项。
再讨论handleKeyboardGroup()。
这个就更好理解了,有几个键组,电脑键盘就能够控制几个键组。(注:这五个键组名字依次为“大字组”,“小字组”,“小字一组”,“小字二组”。“小字三组”)
handleOctive()和handleKeyboardGroup()主要是在调整下拉框的内容,好比键组数目为4时,那么屏幕上有“大字组”,“小字组”,“小字一组”和“小子二组”,这时屏幕上并无“小字三组”,控制键盘所选键组下拉框里再显示“键盘控制小字三组”,就不合适了。
eventHandlers.js
这个文件包含4个下拉框的响应函数。另外,它还包含一些全局变量和全局函数的定义,用于PCEventHandlers.js和mobileEventHandlers.js中的响应函数。
4个onchange响应函数很简单,没什么好说的。
我把这些全局变量和全局函数集中到这里,是为了方便管理与查看,因为是全局的,因此另外两个文件(PCEventHandlers.js和mobileEventHandlers.js)的响应函数照样可使用。
clickOn:鼠标按到琴键上,值变为true;鼠标抬起,值变为false。当鼠标拖动时,利用该值能够判断用户是否在“按着琴键拖动”
positionListener:当鼠标按下并拖动时,positionListener.a用于记录上一个位置的对应音调值,以判断当前位置相对于上一个位置是否变化了琴键(把它定义为Object是为了按引用传递^~^)
noteRecord,rectRecord:当前鼠标点击或拖动的位置会有对应音调和对应琴键区域的两个值,记录于这两个变量,这两个值分别传递到声音和绘图相关函数便可发出声音和颜色变换
noteOnJudge:这是记录键盘上12个音调键按下或抬起的变量,抬起则为0,按下则为1
keyUpAndDownTable:这里面的十二个值记录着键盘上A,W,S,E,D,F,T,G,Y,H,U和J的键盘码,按照顺序,分别表明C,C#,D,D#,E,F,F#,G,G#,A,A#和B这12个音调
computerKeyboardGroup:记录当前电脑键盘控制的键组,中央C键所在键组为0,中央键组左邻居键组为-1,再往左为-2,右边为正,当有4个键组时,相应键组值如图所示:
noteRecordRect:用于触控时,记录某音调对应的琴键区域
getPos:转换坐标
PCEventHandlers.js
这个文件包含着3个鼠标响应函数,和2个键盘响应函数。
对于3个鼠标事件(按下,拖动和抬起),咱们但愿:按下时打开音调,琴键区域涂成彩色;按住并拖动致变换琴键区域时,关闭上一个音调,打开当前音调,将前一个区域涂成黑色或白色,当前区域涂成彩色;抬起时关闭音调,并将当前琴键区域涂回黑色或白色。
打开音调和将当前琴键区绘制成彩色的两个函数以下:
1 myAudio.startNote(note); 2 myCanvas.paintKey(rect, 'click');
关闭音调和将当前琴键区涂回黑色或白色的函数以下:
1 myAudio.stopNote(note); 2 myCanvas.paintKey(rect, 'release');
在以上几个函数中,参数note是一个整形值,范围是24~83,表明音调;参数rect是一个对象,里面包含了记录琴键的区域的数值,和颜色数值,这个对象的结构咱们要到myCanvas()中具体说。
如下语句
1 clickOn = true;
2 clickOn = false;
第1行是在onmousedown()中的语句,第二行是在onmouseup()中的语句。clickOn就是前面eventHandlers.js中的全局变量,clickOn为true时,表明鼠标已经按下而且按到了琴键区域,这时只要鼠标扫过不一样的琴键区域,就会发声。
下面第一个函数能够将鼠标的位置点转换成音调值,而第二个函数能够将音调值转换成相应的琴键区域。
1 myCanvas.positionToNote(pos.x, pos.y), 2 myCanvas.noteToRect(note);
下面这个函数是检测拖动时鼠标位置是否在改变琴键区域,好比鼠标点击到了C键,再拖动到了D键,在鼠标刚刚到达D键时,此时下面的函数返回true,其余时候返回false。按在C键而只在C键区域内移动,并非真正的移动,此时下面的函数时时返回false。此外,当鼠标点移出琴键区或从“外面”移到琴键区域时,也视为改变了琴键区域,下面的函数也会在改变的瞬刻返回true。
1 myCanvas.ifPositionChanged(pos.x, pos.y, positionListener);
对于2个键盘事件(按下和抬起),咱们但愿按下时打开音调,将当前琴键区涂成彩色;抬起时关闭音调,将琴键区域涂回黑白色。
在eventHandlers.js中,咱们定义了keyUpAndDownTable用于按顺序从0~11存放了A,W,S,E,D,F,T,G,Y,H,U和J这些“音调键”的键盘码;还定义了noteOnjudge,在这里noteOnJudge(0) = 1表明A键处于按下的状态,noteOnJudge(4) = 0表明D键处于抬起的状态。noteOnJudge用处是这样的:在有音调键按下时,不容许切换键组,即此时按“K”,“L”,左方向键或右方向键不起做用。这样作的目的是防止“卡键“--键组移走了,音调就没法关闭了。
onkeydown函数有3部分,按下“K“或左方向键,且全部音调键抬起,向左切换键组;按下”L“或右方向键,且全部音调键抬起,向右切换键组;按下音调键,打开音调,琴键区域绘成彩色。
其中的
1 myCanvas.paintIndicator(computerKeyboardGroup);
是绘制指示符的。指示符就是屏幕上当前键组上方的三个红绿蓝色的四分之三圆,用来指示当前键组。
onkeyup函数只有1个部分,抬起音调键,关闭音调,琴键区域恢复到黑色或白色。
mobileEventHandlers.js
这个文件包含着3个触摸响应函数。
上面的两个preventDefault是分别为了阻止手机浏览器上滚动事件和长按弹出菜单事件,这两个事件都会影响使用效果。
3个响应函数分别处理触摸开始,滑动和触摸结束。固然,触摸开始的时候打开音调,琴键涂成彩色;触摸结束时关闭音调,琴键涂成黑色或白色。
重点看一下canvas.ontouchmove这个函数,我以为这是响应函数中最难实现的一个。先贴代码:
1 canvas.ontouchmove = function() { 2 var pos, trues = new Array(); 3 for (var i = 0; i < event.targetTouches.length; i++) { 4 pos = getPos(event.targetTouches[i]); 5 var n = myCanvas.positionToNote(pos.x, pos.y), 6 r = myCanvas.noteToRect(n); 7 if(trues.indexOf(n) < 0) trues.push(n); 8 if(!noteRecordRect[n]) { 9 noteRecordRect[n] = true; 10 myAudio.startNote(n); 11 myCanvas.paintKey(r, 'click'); 12 } 13 } 14 for( var i=24; i < 84; i++) 15 if(noteRecordRect[i] && trues.indexOf(i) < 0) { 16 myAudio.stopNote(i); 17 myCanvas.paintKey(myCanvas.noteToRect(i), 'release'); 18 noteRecordRect[i] = false; 19 } 20 };
函数有两个大部分,分别是4~14行和15~20行的for语句。
event.targetTouches表明屏幕区域的全部触摸点(此外event.changedTouches表明变化的触摸点,注意区分),trues数组会记录此次拖动事件的全部手指激活的琴键的音调,而此前在eventHandlers.js中定义的noteRecordRect数组则是记录的直到上次拖动事件全部手指激活的琴键的音调。那么,第8行的意思是:上次拖动事件手指未到达本琴键区域,可是此次到达了——这就是说手指刚刚触摸本琴键,因此这时打开音调,琴键绘制彩色。第15行的意思是:虽然上次手指触摸了本琴键区域,可是此次却没有——这就是说手指刚刚离开本琴键,因此这时关闭音调,琴键绘制回黑色或白色。
这个“触摸拖动”响应函数,和“鼠标拖动”响应函数不一样的一点在于能够多点拖动。这里不是很好懂,我也不太好叙述出来,你们能够本身琢磨琢磨^ >< ^。
myAudio.js
这个文件里存的就是管理声音的构造函数了,小伙伴们能够看一下,如何借助webAudiofontPlayer库的api,进行声音操做。
你们都知道,js能够用构造函数生成对象,在这里就能够用
1 var myAudio = new MyAudio();
这句来实现。
构造函数内部有一些内部变量,和一些函数。this.init函数就是在main.js中init()调用的声音部分初始化函数,this.importScript会调用this.loadScript,完成引入并解码音源文件的任务,this.setOrGetOctive能够设置或得到当前的八度值,this.startNote和this.stopNote则是打开和关闭单一音调的。
最简单的状况下,webAudiofontPlayer如下列方式实现音调的打开和关闭。
1 var AudioContextFunc = window.AudioContext || window.webkitAudioContext; 2 var audioContext = new AudioContextFunc(); 3 var player=new WebAudioFontPlayer(); 4 player.loader.decodeAfterLoading(audioContext,' 5 _tone_0250_SoundBlasterOld_sf2);//解码 6 var a = player.queueWaveTable(audioContext, audioContext.destination 7 , _tone_0250_SoundBlasterOld_sf2, 0, 12*4+7, 2);//打开音调,最后面三个参数分别是起始播放时间,音调高低,音量
8 a.cancel();//关闭音调
音源文件的加载过程有可能花费一些时间,this.loadScript函数会在url指向的音源文件加载完成后再调用callback函数。咱们能够再this.importScript函数中看到下面这段代码:
1 this.loadScript('../sound/'+ tag + '_sf2_file.js', function() { 2 player.loader.decodeAfterLoading(audioContext, '_tone_' + tag + '_sf2_file'); 3 loadedInstruments[loadedInstruments.length] = tag; 4 document.getElementById('loading').style.display = 'none'; 5 });
在这里咱们在this.importScript中调用了this.loadScript函数,在加载完成" '../sound/' + tag + '_sf2_file.js' "文件后执行后面的函数。后面的函数中,第一句是解码刚刚加载的音源文件;第二句是将已经加载的乐器音源文件记录在loadedInstruments数组中,待下次须要使用该乐器时避免重复加载;第三句是隐藏掉页面上的loading标志,告知用户资源加载完毕,可使用了。
在myAudio.js中还有一个continuousTable,这个数组用来表示乐器的连续性问题。好比鼓,打击一下只会相对瞬时响一声,而且存在回声;但要是口琴就会有一个时间延续问题。因此,若是乐器是连续的,咱们能够先将播放时间设置为999秒,带用户抬起鼠标或键盘时使用cancel()方法,关闭音调;若是乐器是不连续的,咱们能够规定一个时间,只要按下键盘或鼠标,即打开音调,时间到了自动中止,要使它再次打开须要再次激发。