博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
网页音乐制作器(网页钢琴)-- MusicMaker
阅读量:6991 次
发布时间:2019-06-27

本文共 9988 字,大约阅读时间需要 33 分钟。

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

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

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

  网页示范:  , 在浏览器中打开就可以啦(ie,edge除外,手机记得横屏)

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

  最初,学校的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是为了在浏览器上使用MIDI硬件设备的,并不能直接解决我的问题。与是我又花了很长时间,不停地找,无数次想放弃,但是最终,我找到了一个perfect的东西(大神的东西……) 。

  这不是MIDI,MIDI发声原理是主控器(比如MIDI键盘)发送信号,经音序器(Sequencer)处理,使内置音乐播放器调用音源,进而使扬声器发声。所以MIDI传输的是数字符号,用来表示音乐的起伏。这个库就是模仿的这一过程,我们可以通过键盘鼠标手机触摸屏(相当于主控器)进行编辑,然后通过html5的Web Audio Api(相当于Windows的内置音乐播放器)播放音源发声,这里的音源文件,那位大神也已经准备好了,,这里面有一百多种乐器的音源(即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   2   3     4      
5
7 LivePlay 即时演奏 8 9 11
12

MusicMaker

14

LivePlay

15
19
26
33
40
Loading...41
switch to arranger43
44
45 46 47 48 49 50 51 52 53 54

 

 

   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.destination7     , _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

你可能感兴趣的文章
Android210 调试支持 wince6.0系统
查看>>
Android 开发佳站
查看>>
JSR310 时间类型的相互转换
查看>>
Support Vector Machine (2) : Sequential Minimal Optimization
查看>>
过滤器
查看>>
委托和回调函数例子
查看>>
XML与HTML 区别
查看>>
1312:【例3.4】昆虫繁殖(递推算法)
查看>>
继承,多态,抽象,接口
查看>>
C#ADO.NET基础一
查看>>
一个文字横向滚动的JavaScript文档
查看>>
junit整合spring
查看>>
java正则表达式【大全】
查看>>
mac上git安装与github基本使用
查看>>
如何为引用类型如何重写Object.Equals()方法?
查看>>
下拉顶部刷新简单实现
查看>>
Linux的基本配置
查看>>
Django框架之模型层(二)
查看>>
who 查看系统登录用户
查看>>
java语言中application异常退出和线程异常崩溃的捕获方法,并且在捕获的钩子方法中进行异常处理...
查看>>