利用Java技术开发音乐创作辅助软件

摘 要:在科学技术日益进步的今天,各个领域与计算机科学技术融合发展,产生了一片欣欣向荣的发展景象。
利用Java面向对象编程技术开源库多,跨平台移植性好以及自己对音乐知识有所了解的优势,我想帮助无数个像我一样的业余音乐人,所以我设计了一款音乐软件,为喜欢音乐的人开辟一条音乐学习与创作的途径。
该软件目前使用了Java编程语言,JFugue开源工具包,Java Swing GUI技术,MIDI技术。实现了MIDI编辑、解析、保存的功能。论文对所用的技术细节和开发过程进行了详尽的介绍。
关键词:Java;MIDI;JFugue;Java Swin
The Digital Audio Workstation developed by Java
Major: Computer science and technology Class: computer 142 Name: swiss126 Instructor: uOiaH
Abstract: Today, with the progress of science and technology, the integration and development of various fields with computer science and technology has produced a flourishing development scene.
I want to help countless amateur musicians like me, using Java - oriented programming technology, open source libraries, cross platform portability and understanding of music knowledge. So I designed a music software to open up a way of music learning and creation for people who like music.
The software currently uses Java programming language, JFugue Open Source Toolkit, Java Swing GUI technology, MIDI technology. The functions of editing, parsing and saving of MIDI are realized. The technical details and development process are introduced in detail in this paper.
Keywords: Java ; MIDI; JFugue;Java Swing
第1章 项目分析
1.1 现状
在科学技术日益进步的今天,计算机科学技术是其中比较突出的一个分支,各个领域与计算机科学技术融合发展,产生了一片欣欣向荣的发展景象,移动支付,电子商务,虚拟现实,人工智能处处可见。
音乐领域也是如此,虚拟歌手的出现把音乐发展推到了一个新的高度,自从VOCALOID虚拟歌手诞生以来,吸引了无数粉丝,也吸引了无数音乐人为她作曲编曲。就拿中文v家来说,从2012年洛天依出道开始,到现在也就短短6年,创造了784首殿堂曲,其中有不少脍炙人口的优秀作品流传在各个地方。这对于乐坛来说是不小的推动。
然而业余音乐人想要进行音乐创作,总会遇到各种困难,比如入门难,学习培训费用高等各种困难。为此我们打算设计一款音乐软件,为喜欢音乐的人开辟一条音乐学习与创作的途径。该软件包含音乐创作辅助功能,以及简易的乐理知识介绍,可以方便音乐人或者音乐爱好者学习与创作。
1.2 需求分析
1、该软件主要面向业余音乐爱好者,此类人群大多数没有专业的电脑,所以程序需要足够简洁轻量,在各种不同的系统环境中都可以正常运行。Java编程语言拥有面向对象编程、移植性良好、安全性高、拥有并发机制、支持可视化图形界面等优点,使用我选择使用Java开发项目。
2、目前主流的音乐软件大多数是外国人使用C++开发,其中一些软件体量庞大,安装复杂,操作繁琐,不利于业余音乐爱好者创作和学习。因此,项目需要做到足够精简,足够好用,在符合中国用户的操作习惯的基础上实现国际化。
3、根据音乐人创作音乐需要的步骤来看,如图 1‑1所示,软件需要包含作曲编曲(包括创作辅助功能)、混音混缩、读写文件(包括打谱,导入,导出工程文件,发布作品)等模块。
4、需要支持主流的音乐工程文件以及特殊人群常用的工程文件,实现支持MIDI,VSTi等通用性强的技术标准,这样可以方便用户与其他软件用户进行交流与学习。

图 1‑1 音乐人作曲编曲的流程示意图
1.3 设计方案
根据软件需求分析,仔细研究思考以后,决定使用以下设计方案:
1、采用Java语言开发项目,根据实际需求选择合适的插件与开发包,减轻开发工作量。
2、界面足够精简清爽,允许用户设置界面的背景图片以及系统颜色,语言包等选项。
3、软件需要包含作曲编曲(包括创作辅助功能)、混音混缩、读写文件(包括打谱,导入,导出工程文件,发布作品)等模块。采用快速原型模型与渐增模型的方法开发软件,用户可以随时看到开发进度,并且可以对软件提出建议,这样开发出来的软件会更符合用户的需求,以及使用习惯。
4、努力支持其他软件的工程文件以及通用技术标准,方便用户交流与学习。
1.4 研发规划
本项目打算分3个阶段进行研发:
1、技术试验阶段,毕设期间主要是摸索探讨实现计算机音乐创作软件需要的用到技术,以及需要实现的技术标准,尽力实现核心模块,为之后进一步开发打下扎实的基础。
2、研发阶段,工作1-2年期间,利用空余时间对毕设作品已实现的功能进行算法优化,未实现的功能继续实现并且根据既定标准进行算法优化。
3、运营维护阶段,项目开发得到可以稳定运行功能齐全的软件后,打算寻求运营商代理运营软件。期间本人将会负责软件的技术支持及版本更新。同时利用制作软件学到的技术,进行衍生产品的研发。比如说可以投入音乐教学的APP,可以提醒用户工作学习的虚拟对象等应用领域研发。
第2章 技术简介
2.1 MIDI
MIDI(Musical Instrument Digital Interface)乐器数字接口,是20 世纪80 年代初为解决电声乐器之间的通信问题而提出的。MIDI是编曲界最广泛的音乐标准格式,它用音符的数字控制信号来记录音乐。一首完整的MIDI音乐只有几十KB大,而能包含数十条音乐轨道。
2.2 JFugue
JFugue 使用字符串来描述音乐信息,然后将其转换成 MIDI 指令并通过 Java Sound 接口对其进行处理,因此他可以播放、读取、保存MIDI文件。他的 API 非常的容易理解和使用,所以我们使用JFugue来进行MIDI编程。
2.3 Swing
Swing 是一个为Java设计的GUI工具包,他是JAVA基础类的一部分。Swing提供许多比AWT更好的屏幕显示元素。它们用纯Java写成,所以同Java本身一样可以跨平台运行。
2.4 多线程
采用了多线程技术可以让应用程序更好地利用系统资源。其主要优势在于充分利用了CPU的空闲时间片,可以用尽可能少的时间来对用户的要求做出响应,使得进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性。
第3章 软件设计
3.1 软件功能
软件功能有作曲编曲(包括创作辅助功能)、混音混缩、读写文件(包括打谱,导入,导出工程文件,发布作品)等模块,目前主要介绍作曲编曲模块。
作曲编曲模块分为工程设置,钢琴窗,音轨编辑器,音轨设置四个子模块构成,主要功能就是辅助用户完成作曲编曲的操作,必要的情况下应该给用户乐理知识的提示,帮助乐理基础不扎实的用户创作好听的作品。
混音混缩模块主要就是混音台和效果器,该模块辅助用户完成歌曲的混音工作,得到可以播放的音频伴奏或者歌曲试听。
读写文件模块负责数据的本地持久化,保存、备份用户的工程文件,导出做好的作品,如果有可能还需帮助用户将作品快速的发布到音乐平台。
3.2 软件性能
1. Windows7 64位、JRE1.8、4GB内存、500G硬盘、Intel Celeron CPU 847@ 1.10GHz
环境中流畅运行
2. 稳定的播放、读写、解析、编辑MIDI文件
3. 钢琴窗能支持1000个音符的音序流畅显示和操作
4. 音轨编辑器支持32音轨编辑
3.3 软件架构
如图 3‑1所示,本软件采用MVC三层架构,Model层利用JFugue实现了MIDI数据结构建模,View层使用Java Swing以及Window Builder插件搭建,Ctrl层利用多线程技术控制程序运转。

图 3‑1 工程分层分包图
3.3.1 View层
View层基于Java Swing开发,通过继承重写Java Swing的控件实现自定义控件的效果,类图如图 3‑2所示。由于作曲编曲软件的太小众了,Java Swing并没有提供相关的控件,钢琴窗,音轨编辑器需要自己动手编写控件。为了实现这种大规模集成的控件,我决定使用Eclipse的Window Builder插件(插件界面如图 3‑3所示)进行辅助设计,设计完界面再进行代码优化完善。

图 3‑2 View层 部分类图

图 3‑3 WindowBuilder界面
View层界面由多个内部窗口和控件构成,采用异步刷新的方式可以减少系统资源占用,提高应用程序的效率,界面的刷新机制如图 3‑4所示

图 3‑4界面刷新机制
3.3.2 Control层
1. MIDI解析
软件需要加载MIDI中的数据,并且把他们显示出来,这就涉及到了MIDI解析的问题。本人在JFugue的API基础之上设计了一个MIDI解析方法,如图 3‑5所示。

图 3‑5 MIDI解析
2. 多线程播放
如图 3‑6所示,在MIDI底层播放机制中,MIDI序列被分成了多个音轨,这些音轨有多线程控制,在对应的时间发送短信息给音序器,音序器根据发送的信息进行对应的处理,比如播放音符,改变音色,切换MIDI通道等,这样的机制需要考虑到线程同步的问题。

图 3‑6 MIDI播放机制
在JFugue的基础上,我设计了一套多线程播放机制,让播放器更加能够适合我们的需求。
这套机制的播放及其状态转换如图 3‑7、图 3‑8、图 3‑9所示。

图 3‑7 SRealtimePlayer播放机制

图 3‑8 ProjectPlayer播放机制

图 3‑9 播放状态转换机制
3.3.3 Model层
Model层根据MIDI文件的数据结构,在JFugue的基础上设计了这套模型。模型中包含MidiDictionary、Note、Sequence、Track这四个类。为了与JFugue的类相区别,我给Note、Sequence、Track加上了前缀My。MyNote是音符类,表示乐曲中的一个音符。MySequence是音序类,表示乐曲中的一个片段。MyTrack是音轨类,表示乐曲中的一个音轨。其关系如图 3‑10所示,数据字典如表 3‑1、表 3‑2、表 3‑3所示。
表 3‑1 MyTrack 数据字典

表 3‑2 MySequence 数据字典

表 3‑3MyNote 数据字典

第4章 功能实现
4.1 作曲编曲
这个模块是整个软件的核心模块,软件中的所有功能都需要围绕他们运转。
目前界面主体采用在JPanel上绘制图形图像的方法画出钢琴窗与音轨编辑器的网格,用JTextField控件模拟音符和音序,用滚动条控制界面缩放移动,用复位按钮实现界面快速还原原始比例。
4.1.1 钢琴窗
钢琴窗界面如图 4‑1所示,上面可以很清楚的看到一个音序内包含的音符,并且可以用工具栏中的工具对他们进行修改。

图 4‑1 钢琴窗界面设计
钢琴窗由PianoRollFrame、PianoPanel、NoteField、ParameterCanvas、PianoCanvas等五个类共同实现,
其中PianoRollFrame是模块的主类;PianoPanel是钢琴窗左侧的钢琴键盘,可以显示音高,方便用户操作;NoteField是实现音符控件的类,支持音符的创建,删除,修改,移动。ParameterCanvas是参数编辑面板的控件类,PianoCanvas是音符编辑面板的控件类。
如表 4‑1所示,PianoRollFrame类包含以下变量和常量:
表 4‑1 PianoRollFrame类包含的变量和常量

4.1.2 界面绘制方法
PianoPanel直接画出准备好的钢琴窗图片,并且在上面标上音名。ParameterCanvas和PianoCanvas重载paintComponent方法,用java.awt.Graphics自带的绘图方法绘制线条,给线条的颜色加点透明效果,可以让画出来的网格更好看。
用PianoRollFrame的变量dX,dY,dW,dH控制PianoPanel、ParameterCanvas、PianoCanvas的绘制线条位置,长度以及颜色,可以实现好看的效果。
控制方法如下:

用grade控制线条的级别,不同级别的线条粗细不同,alpha控制线条的颜色的透明度,当纵轴拉长的时候,横线会变粗,纵轴拉短的时候,横线变细,用dX,dY,dW,dH根据平面直角坐标系图形的缩放就可以计算出线条应该落在的位置。最后用for循环可以把横线画满钢琴窗。
纵线的绘图方法类似,但是由于不同缩放比例下显示纵轴的情况不一样,当横轴的比例缩得太小的时候,一些纵线会隐藏起来,所以需要加个判断是否绘制出纵线。
绘制方法如下:

4.1.3 界面刷新机制
拖动滚动条控件改变dX,dY,dH,dW四个变量之一时或者添加删除修改音符时,钢琴窗会自动刷新,加载需要显示的音符,并把编辑过的音符保存到对应的音序中。
刷新方法如下:

4.1.4 参数编辑器
参数编辑器是钢琴窗底部的一个控件,用于编辑音符参数,如图 4‑2所示。通过JFugue包提供的方法可以对MIDI的参数控制器进行设置,在JFugue中,用Token记号“:PW”设置弯音轮,记号“:CE”设置参数编辑器,“a”记号设置音符初始力度,“d”记号设置音符结束力度,通过控制以上参数可以做出美妙动听的音乐。比如JFugue Pattern “:CE(93,127) :CE(8,127) E3wa100d0”表示 设置控制器第93个参数,值为127,设置控制器第8个参数,值为127,播放音符E3,初始力度100,结束力度0。再如“:PW(0,64)”表示设置弯音轮,值为高位是0,低位是64。

图 4‑2 参数编辑器
4.1.5 音轨编辑器
音轨编辑器的界面如图 4‑3所示,其工作原理与钢琴窗类似,就是通过绘制线条,改变控件位置实现特定效果。音轨编辑器由SequenceField、TrackCanvas、TrackFrame、TrackMsgEditor、TrackMsgPanel等五个类共同实现。

图 4‑3音轨编辑器界面设计
如表 4‑2所示,TrackFrame类包含以下变量和常量:
表 4‑2 TrackFrame类包含的变量和常量

控件的绘图机制与刷新机制和钢琴窗类似,因此不再赘述。
4.2 工程设置
音乐作品不是千篇一律的,节拍,调式调性,曲速,乐器都会根据音乐人的需要发生改变。这个模块就是通过MIDI接口实现帮助音乐人设置音乐项目的功能。如图 4‑4图 4‑5所示,用户可以根据自己需要设置作品的曲速,节拍,MIDI端口号,演奏乐器,歌曲长度。

图 4‑4 工程设置

图 4‑5 音轨设置
设置模块由ProjectEditor和TrackMsgEditor、Main这三个类构成,其中TrackMsgEditor从属于音轨编辑器模块。在MIDI标准中,有16个MIDI通道(其中第10个通道是鼓映射通道,用来演奏鼓乐器),128种乐器,和一个GM鼓映射。用JFugue包可以轻松的对他们进行设置,用JFugue的Token记号I,可以设置乐器,比如记号“I0”就是选择使用MIDI标准中的0号乐器——钢琴。另外可以用记号V来设置MIDI通道,“V0”表示0号通道,“V1”表示1号通道,依次类推直到“V15”表示15号通道。其中“V9”是鼓映射通道,用来演奏鼓乐器。用TIME记号可以设置节拍,T记号设置曲速(单位是BPM),Key记号设置调性,例如JFugue Pattern “TIME:4/4 KEY:Gmaj T100”表示歌曲的节拍是4/4拍,调性是G大调,曲速是100BPM。音轨颜色的设置主要是为了方便音乐人区分不同音轨,提高作曲编曲的效率。
4.3 文件读写
在音乐编辑软件中,文件读写是实现数据本地持久化的主流方法。音乐人可以通过保存文件的方式把自己的作品永远的保存起来。如图 4‑6所示,本系统目前支持MIDI标准格式文件(后缀为.mid)未来将会支持其他的主流文件格式以及特殊人群通用的文件格式。

图 4‑6 文件读写模块
4.3.1 打开MIDI文件
在JFugue中,打开文件只需要调用方法MidiFileManager.loadPatternFromMidi(File file)即可实现最基本的打开操作。然而软件需要加载MIDI中的数据,并且把他们显示出来,这就涉及到了MIDI解析的问题。目前JFugue中提供的MIDI解析方法不够完善,pattern.getTokens()只能得到一些拆分过的Token记号,光靠这些记号无法把打开的MIDI文件显示到界面上。于是本人写了一个解析Token的方法来实现进一步的MIDI解析。解析方法如下:
public void LoadMidi(Pattern pattern){//解析读取到的midi,并在程序中创建相应的音序,音轨,音符
System.out.println("解析MIDI:"+pattern);
TrackFrame.Reset();
/**
* 解析到的记号列
*/
List<Token> tokens=pattern.getTokens();
/**
* 新建音序列
*/
List<MySequence> sequenceList=new ArrayList<MySequence>();
/**
* 新建音序
*/
MySequence sequence = new MySequence(0,0, Main.length,(int) (TrackFrame.DefaultHeight), "未命名音序");
/**
* 新建音符
*/
MyNote note=new MyNote(0, 0, 0, 0, "", 0, 0, 0);
/**
* 音符计时器,音轨计时器
*/
double NoteTimer=0d,TrackTimer=0d,Length=0d;
/**
* 控制参数(CE)集
*/
Map<Integer,String> ParameterList=new HashMap<Integer,String>();
boolean IsFirstNote=true,IsJump=false;
int sequencecount=0,midiport=0,instrument=0;
for(Token t:tokens){
switch(t.getType()){
case VOICE://端口号
if(sequence.getPattern().length()>0){//再次遇到V记号的时候,上一个音轨的数据解析完成
sequencecount++;
sequence.setTxt(MidiDictionary.InstrumentType.get(sequence.getInstrument()));
sequence.set_width((int) NoteTimer*Main.SNote*4-sequence.get_x()+1);
if(sequence.getNoteList().size()>0){
if(sequence.getMIDIPort()==9)sequence.setTxt("GM鼓音色");
System.out.println(sequence+"\n"+sequence.getPattern());
sequenceList.add(sequence);
}
sequence.setPattern("");
if(NoteTimer>Length)Length=NoteTimer;
sequence = new MySequence(0,0, Main.length,(int) (TrackFrame.DefaultHeight), "未命名音序");
sequence.setMIDIPort(midiport);
sequence.setInstrument(instrument);
IsFirstNote=true;
NoteTimer=0d;
}
if(sequencecount<32)sequence.set_y(sequencecount);
sequence.set_x((int) (TrackTimer*Main.SNote*4));
sequence.set_height(TrackFrame.DefaultHeight);
midiport=Integer.parseInt(t.getPattern().toString().substring(1));
break;
case LAYER:
sequence.setPattern(sequence.getPattern()+" "+t.getPattern());
break;
case INSTRUMENT://乐器号
if(sequence.getPattern().length()>0){//再次遇到I记号的时候,说明上一个音轨的数据解析完成
sequence.setTxt(MidiDictionary.InstrumentType.get(sequence.getInstrument()));
sequence.set_width((int) NoteTimer*Main.SNote*4-sequence.get_x()+1);
if(sequence.getNoteList().size()>0){
if(sequence.getMIDIPort()==9)sequence.setTxt("GM鼓音色");
sequencecount++;
sequence.set_y(sequencecount);
sequenceList.add(sequence);
}
sequence.setPattern("");
if(NoteTimer>Length)Length=NoteTimer;
sequence = new MySequence(0,0, Main.length,(int) (TrackFrame.DefaultHeight), "未命名音序");
sequence.setMIDIPort(midiport);
sequence.setInstrument(instrument);
IsFirstNote=true;
NoteTimer=0d;
}
sequence.set_x((int) (TrackTimer*Main.SNote*4));
sequence.set_height(TrackFrame.DefaultHeight);
instrument=Integer.parseInt(t.getPattern().toString().substring(1));
break;
case TEMPO://曲速
Main.BPM=Double.parseDouble(t.getPattern().toString().substring(1));
break;
case KEY_SIGNATURE://调
//System.out.println("Pattern:"+t.getPattern()+"\tType:"+t.getType());
break;
case TIME_SIGNATURE://节拍
Main.Meter=Integer.parseInt(t.getPattern().toString().split("/")[0].substring(5));
Main.Part=Integer.parseInt(t.getPattern().toString().split("/")[1]);
break;
case BAR_LINE:
sequence.setPattern(sequence.getPattern()+" "+t.getPattern());
break;
case TRACK_TIME_BOOKMARK://音符开始时间
TrackTimer=Double.parseDouble(t.getPattern().toString().substring(1));
IsJump=true;
sequence.setPattern(sequence.getPattern()+" "+t.getPattern());
break;
case TRACK_TIME_BOOKMARK_REQUESTED:
sequence.setPattern(sequence.getPattern()+" "+t.getPattern());
break;
case LYRIC:
sequence.setPattern(sequence.getPattern()+" "+t.getPattern());
break;
case MARKER:
sequence.setPattern(sequence.getPattern()+" "+t.getPattern());
break;
case FUNCTION:
if(t.getPattern().toString().contains("CE"))
ParameterList.put(Integer.parseInt(t.getPattern().toString().split(",")[0].substring(4)),t.get
Pattern().toString().split(",")[1].substring(0, t.getPattern().toString().split(",")[1].length()-1));
sequence.setPattern(sequence.getPattern()+" "+t.getPattern());
break;
case NOTE:
sequence.setPattern(sequence.getPattern()+" "+t.getPattern());
Note jnote = null;
try{
jnote=new Note(t.getPattern().toString());
}catch(Exception e){//如果无法识别,很可能是鼓
sequence.setMIDIPort(9);
sequence.setSequenceType(App.Rhythm);
sequence.setTxt("GM鼓音色");
String temp=t.getPattern().toString();
temp=temp.replace("["+temp.split("]")[0].substring(1)+"]",MidiDictionary.RhythmToKey
.get(temp.split("]")[0].substring(1)));
jnote=new Note(temp);
}
if(IsJump){
note.set_x((int) (TrackTimer*Main.SNote*4));
NoteTimer=TrackTimer+jnote.getDuration();
}else{
note.set_x((int) (NoteTimer*Main.SNote*4));
NoteTimer+=jnote.getDuration();
}
note.set_y(119-jnote.getValue());
note.set_width((int) (jnote.getDuration()*Main.SNote*4));
note.set_height((int) PianoRollFrame.DefaultKey);
note.setParameterList(ParameterList);
if(jnote.getValue()>0){
if(IsFirstNote)sequence.set_x(note.get_x());
sequence.updateNote(note);
}
IsFirstNote=false;
IsJump=false;
note=new MyNote(0, 0, 0, 0, "", 0, 0, 0);
ParameterList=new HashMap<Integer,String>();
TrackTimer=0d;
break;
default:
sequence.setPattern(sequence.getPattern()+" "+t.getPattern());
break;
}
}
sequencecount++;
sequence.setTxt(MidiDictionary.InstrumentType.get(sequence.getInstrument()));
sequence.set_width((int) NoteTimer*Main.SNote*4-sequence.get_x()+1);
if(sequence.getNoteList().size()>0)sequenceList.add(sequence);
sequence.setPattern("");
if(NoteTimer>Length)Length=NoteTimer;
NoteTimer=0d;
List<SequenceField> sequenceFieldList=new ArrayList<SequenceField>();
for(MySequence s:sequenceList){
SequenceField sequenceField=new SequenceField(s.getTxt(),s.get_x(),s.get_y(), s.get_width(), s.get_
height());
sequenceField.sequence=s;
TrackFrame.trackmsgpanel[s.get_y()].track.setInstrument(s.getInstrument());
TrackFrame.trackmsgpanel[s.get_y()].track.setMIDIPort(s.getMIDIPort());
TrackFrame.trackmsgpanel[s.get_y()].track.setTrackType(s.getSequenceType());
TrackFrame.trackmsgpanel[s.get_y()].track.setTxt(s.getTxt());
TrackFrame.trackmsgpanel[s.get_y()].Background=App.FieldColor;
sequenceFieldList.add(sequenceField);
//System.out.println(s);
}
Main.length=(int)Length+1;
PianoRollFrame.Blank.setText(Main.Meter+"/"+Main.Part+"拍 "+Main.length+"节 BPM="+Main.BP
M);
TrackFrame.Blank.setText(Main.Meter+"/"+Main.Part+"拍 "+Main.length+"节 BPM="+Main.BPM);
TrackFrame.sequence=sequenceFieldList;
MainFrame.desktopPane.repaint();
MainFrame.InternalFrame.get(App.$Track).setVisible(true);
MainFrame.Stbtn[2][App.$Track].setSelected(true);
TrackFrame.Repaint();
}
4.3.2 保存MIDI文件
JFugue自带的保存文件方法是MidiFileManager.savePatternToMidi(PatternProducer patternProducer, File file) throws IOException,这个方法可以把一段JFugue Pattern保存成MIDI文件。所以我要做的就是把软件中的数据转换成JFugue Pattern,然后再调用这个方法保存MIDI文件。
根据JFugue的API可以知道不同记号的意思,于是我写了一些Model模型,他们有得到JFugue Pattern的方法。为了和JFugue的模型区分开来,我自己的模型类名前面加了前缀“My”。
MyNote的获取Pattern方法如下:
/**
* 得到音符的JFugue Pattern
* 示例:@1.0625 :CE(4,54) :CE(2,51) :CE(33,39) :CE(0,86) C#4hs
*/
public String getPattern(){
pattern=get_xString()+" "+getParameterString()+" "+getNote()+getLength();
return pattern;
}
/**
* 得到音符的长度
*/
public String getLength(){
String temp="";
int length=_width;
Stack<Boolean> s=new Stack<Boolean>();
do{
s.push(length%2>0);
length=length/2;
}while(length>0 && s.size()<5);
for(int i=0;i<length;i++)
{
temp+=lengthNote[0];
}
int part=s.size();
for(int i=0;i<part;i++)
{
if(s.pop())temp+=lengthNote[lengthNote.length-part+i];
}
return temp;
}
/**
* 得到音符开始播放的时间
*/
public String get_xString(){
return "@"+_x/32.0;
}
/**
* 得到音符的参数
*/
public String getParameterString(){
String temp="";
Set<Integer> keySet=ParameterList.keySet();
for(int key:keySet)
{
temp+=" :CE("+key+","+ParameterList.get(key)+") ";
}
return temp;
}
/**
* 得到音高
*/
public String getNote(){
switch(_y%12)
{
case 0:return "B"+(9-_y/12);
case 1:return "A#"+(9-_y/12);
case 2:return "A"+(9-_y/12);
case 3:return "G#"+(9-_y/12);
case 4:return "G"+(9-_y/12);
case 5:return "F#"+(9-_y/12);
case 6:return "F"+(9-_y/12);
case 7:return "E"+(9-_y/12);
case 8:return "D#"+(9-_y/12);
case 9:return "D"+(9-_y/12);
case 10:return "C#"+(9-_y/12);
case 11:return "C"+(9-_y/12);
}
return null;
}
MySequence和MyTrack获取Pattern的方法只需要把里面包含对象的Pattern拼接起来即可,得到所有Pattern以后通过JFugue自带的输出方法就能把Pattern写到MIDI文件上面。
4.4 走带
播放模块主要由走带面板和实时播放器构成。通过多线程机制,可以实现播放的时候指针跟着移动,同时不影响用户继续操作软件。如图 4‑7图 4‑8所示,走带面板上有播放,暂停,停止,循环按钮,播放指针和一个时间显示器构成,点击时间显示器可以切换显示模式。目前有两种显示模式:一种是时间模式,就是显示当前播放的时间(分:秒、毫秒),另一种是节拍模式,显示当前播放的位置是第几节第几拍。

图 4‑7 走带面板与播放指针(时间模式)

图 4‑8走带面板与播放指针(节拍模式)
4.4.1 多线程播放机制
JFugue已经把MIDI底层的播放机制封装成了两种播放器,一种是Player,就是普通的MIDI播放器,只支持最基本的播放功能。另一种是RealtimePlayer,可以根据自己的需要发送MIDI信息给他,进行实时的播放控制。这里我们用到的是RealtimePlayer。
对JFugue的Player进行进一步的封装,让他更加适合于音乐辅助软件。封装后的SMidiPlayer和SRealtimePlayer具有更好的多线程控制,可以防止播放时主程序不能响应用户操作的情况发生。
SMidiPlayer的多线程机制很简单,就是另开一个线程进行播放操作。SRealtimePlayer的多线程处理机制考虑到MIDI信号可能会太多,一下子无法及时处理的问题,设计了堆栈暂时储存未播放的音符。播放线程每播放一个音符就弹出一个音符,直到播放完成为止。具体实现代码如下:

ProjectPlayer类实现了对SRealtimePlayer的时序控制,从而实现走带功能。具体控制代码如下所示:

4.4.2 状态控制机制
播放状态有播放,暂停,停止,循环四种,他的控制状态方法如下:

第5章 系统测试
5.1 测试方案设计
在软件开发过程中,为了保障编码完成的部分模块可以正常运行,需要进行模块测试。我设计了一些测试方法来测试软件中已经完成的模块运行情况是否符合预期,从而验证这些模块是否能正常运行,是否有必要对他们重新编码。为了达到良好的测试效果,拟定的测试要求如下:
1. 测试环境与大多数用户系统环境相似:Windows7 64位、JRE1.8、4GB内存、500G硬盘、Intel Celeron CPU 847@ 1.10GHz
2. 测试内容包含所有已经编码完成的模块功能以及可能出现的Bug和错误。
3. 测试报告尽可能的详细,测试完成后可以对发现的问题进行汇总分析。
5.2 测试用例设计
根据软件的功能及模块,划分了以下几个测试点,分别是:钢琴窗,音轨编辑器,走带面板,文件读模块。采用黑盒测试策略,根据可能出现的错误准备测试用例。在钢琴窗模块测试中进行编辑音符、界面刷新、发音预览测试,音轨编辑器中进行编辑音轨、编辑音序、打开钢琴窗的测试,走带面板中进行播放状态的测试,文件读写模块中进行新建、打开、保存MIDI文件的测试,测试用例及测试情况如表 5‑1所示。
表 5‑1 测试用例及测试情况

第6章 结论与展望
通过这次项目,我学会了如何开发大规模的项目,以及如何查找资料,如何解决问题,如何利用他人写好的Jar包和API文档,实例程序进行项目开发。另外我对MIDI的文件格式,运行机制,技术标准也有所学习理解,为进一步开发和完善项目打下了扎实的技术基础和理论基础。
进一步的开发计划中我将会利用空余时间对毕设作品已实现的功能进行算法优化,未实现的功能继续实现并且根据既定标准进行算法优化,努力把混音模块、歌声合成模块和作曲编曲辅助模块尽早完成。
参考文献:
[1] 戴歆. Java Swing程序开发.精通Java Swing程序设计[M]. 武汉商业服务学院信息工程系 湖北武汉430056
[2] 杨军. MIDI消息和标准MIDI文件格式剖析及应用. 武汉商业服务学院基础课部
[3] 电子歌声合成软件VOCALOID应用探讨[J]. 杨心祎.音乐时空. 2015(06)
[4] 多媒体技术基础[M]. 清华大学出版社 , 林福宗编著, 2000
[5] JFugue-Java API for music programming[J]. Koelle D.
[6] The Complete Guide To JFugue-v1[J]. David Koelle
[7] 牛育谦. VSTi陕北唢呐插件的设计与实现[D]. 电子科技大学, 2015.
[8] Gunadi M K, Eng L M, Wijaya H K. MIDI CompoSITion Tools dengan menggunakan JFugue Java API[J].
[9] 田梅, 刘瑶, 周冰颖,等. 结合Kinect与MIDI的和声辅助训练系统[J]. 计算机应用与软件, 2015(8):68-71.
[10] Newmarch J. MIDI Java Sound[M] Linux Sound Programming. Apress, 2017.
[11] JFugue[M]. Betascript Publishing, 2010.
[12] 洛伊. Java Swing[M]. 清华大学出版社, 2004.
附 录
附录A MIDI参数控制
附表 1 MIDI中部分参数控制

附录B MIDI标准音色(略)
附表 2 MIDI标准音色
附录C MIDI标准鼓映射
附表 3 MIDI标准鼓映射

附表 4相关词汇中英文对照
