网页音乐制做器(网页钢琴)-- MusicMaker

  我今天要和你们分享的是一个我本身写的音乐网页小程序,这个网页程序主要分为两个部分--即时演奏(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/webaudiofontcanvas

  这不是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()方法,关闭音调;若是乐器是不连续的,咱们能够规定一个时间,只要按下键盘或鼠标,即打开音调,时间到了自动中止,要使它再次打开须要再次激发。

相关文章
相关标签/搜索