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

  我今天要和大家分享的是一个我自己写的音乐网页小程序,这个网页程序主要分为两个部分--即时演奏(LivePlay)和编曲(Arranger)。即时演奏就是指按下鼠标/键盘/手机屏幕就可以即刻发声,编曲是指提前写好“谱子”然后播放。

  这个音乐程序现在仅有网页版,由于我使用Javascript(和HTML,CSS)写成,所以理论上将来它可以移植到Android和iOS上,也可以改成电脑程序,当然也可以改装成微信小程序!

  我是一个Javascript和Web初学者,这个音乐小程序并不复杂,所以如果有喜欢音乐,或在学习Web前端,学习canvas绘图的朋友,大家可以一起探讨程序的机理,体悟美妙的音乐!

  网页示范: https://sien75.github.io/MusicMaker/liveplay , 在浏览器中打开就可以啦(ie,edge除外,手机记得横屏)

  我的github主页就是 https://github.com/sien75,看完整代码来这里就可以,欢迎加星,不胜感激^ >< ^

  最初,学校的C++课程有写程序的课题任务,我萌生了做一个编曲&即时演奏的音乐程序的念头,于是我在网上不断查找,找到了MIDI(Musical Instrument  Digital Interface,乐器的数字接口)这玩意。使用Windows的MIDI消息api--midiOutShortMSG(...),可以发送MIDI消息,然后Windows利用自带的MIDI音色库生成声音。我花费了一个月的时间,用MFC实现了一个简陋的音乐程序。之后,我想进一步把这个程序写下去,使程序更完善,但是我发现自己写的烂代码自己根本不愿回顾……

  而且MFC是一个比较老的东西了,所以我想丢掉之前的代码,重新写一个程序(话说在我不停地“备份-格式化磁盘-换系统”中,那份原始代码终于被我删掉了……)。我想我不是已经会c++了嘛,所以我最初尝试用Qt写。然而我发现Qt没有关于MIDI的api,我也在网上搜索了好一阵子,也没有找到合适的第三方库,于是就不了了之了。

  还有我想实现跨平台的程序,既然Qt & C++不能用了,我想继续用C#写下去。原因如下:1 C#看起来和C++挺像的,应该容易学习;2 VisualStudio + C#号称天下无敌宇宙第一,且跨平台很轻松;3 C#也可以使用Windows的MIDI api,我不用再愁发不出声的问题了;4 看看“C#”这名字,命名人肯定很喜欢音乐,这个语言写音乐程序肯定很适合。

  然而之后再次放弃,具体原因忘记了,可能是我一直想学习Web安全领域,所以我迫不及待要开始前端之路了。于是花费了一些时间学习HTML,学习CSS,学习Javascript(强烈推荐《Javascript高级程序设计》)。

  听说w3c有个Web MIDI Api,我想:何不用这个东西实现音乐程序呢?而且这个浏览器本身就是跨平台的,这样正好符合我的要求。然而Web MIDI Api是为了在浏览器上使用MIDI硬件设备的,并不能直接解决我的问题。与是我又花了很长时间,不停地找,无数次想放弃,但是最终,我找到了一个perfect的东西(大神的东西……) https://github.com/surikov/webaudiofont

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

转载于:https://www.cnblogs.com/sien75/p/8592781.html

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我可以为您提供一些思路。您可以使用PHP的音频库或扩展,如PHP音频处理库(PHP Audio Processing Library)或PHP SoX扩展来实现这个程序。以下是一个简单的实现方法: 1. 首先,您需要定义三个类来表示不同的乐器:Erhu、Piano和Violin。每个类都应该有一个play()方法,该方法将播放相应乐器的声音。 2. 在每个类的play()方法中,您需要使用PHP音频库或扩展来加载相应乐器的声音文件,并将其播放。 3. 接下来,您需要创建一个Musician类,该类将表示乐手。该类应该有一个playInstrument()方法,该方法将接受一个乐器对象作为参数,并使用该对象的play()方法来播放该乐器的声音。 4. 最后,您可以创建一个简单的脚本来实例化Musician和乐器对象,并调用playInstrument()方法来播放相应的乐器声音。 下面是一个示例代码: ```php <?php // 定义乐器类 class Erhu { public function play() { // 使用PHP音频库或扩展来播放二胡声音 } } class Piano { public function play() { // 使用PHP音频库或扩展来播放钢琴声音 } } class Violin { public function play() { // 使用PHP音频库或扩展来播放小提琴声音 } } // 定义乐手类 class Musician { public function playInstrument($instrument) { $instrument->play(); } } // 实例化乐手和乐器对象,并播放音乐 $musician = new Musician(); $erhu = new Erhu(); $piano = new Piano(); $violin = new Violin(); $musician->playInstrument($erhu); // 播放二胡声音 $musician->playInstrument($piano); // 播放钢琴声音 $musician->playInstrument($violin); // 播放小提琴声音 ?> ``` 希望这可以帮助您开始编写程序。如果您有任何问题或需要更多帮助,请随时向我提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值