使用Java/Kotlin编写音乐:JFugue
原文:WRITING MUSIC IN JAVA: TWO APPROACHES
简介
音乐软件能够表达音乐思想,必须是人类可读和计算机可读的。 现代乐谱记谱法具有极强的表现力,能够在一个紧凑的空间内传达节奏、旋律、和声以及各种演奏指令。 不幸的是,作为一种图形化的、人类可读的记谱法,乐谱并不能很好地转化为计算机。 一个单独的记谱系统,即特定领域语言(DSL),对于计算机能够处理音乐是必要的。 此外,我们还需要能理解这种DSL的工具,并允许我们对音乐进行操作。
WRITING MUSIC IN JAVA: TWO APPROACHES介绍了两个开源的Java库,它们使用两种不同的符号,以计算机友好的ASCII格式表达音乐信息。 这两个库都可以通过计算机扬声器以MIDI序列的形式播放曲子,但在其他功能上有所不同。
本文只涉及JFugue,因为后面那个abc4j并不太完善的样子。文章的内容部分翻译自WRITING MUSIC IN JAVA: TWO APPROACHES,版权归于Lance Finney。
文章仅仅把JFugue更新到5.0版,编程采用Kotlin完成。
JFUGUE
JFugue是一个LGPL许可的开源库,用于 “为音乐编程而不需要复杂的MIDI”。 它有自己的符号,只用ASCII字符表示音乐,提供MIDI文件的输入/输出,并允许以编程方式操作音乐。
为了演示JFugue的功能,我们将使用童谣 "Itsy Bitsy Spider "的变体。
JFugue的第一个演示主要显示了歌曲旋律的基本记号。
import org.jfugue.player.Player
fun main() {
val player = Player()
player.play( // "Itsy, bitsy spider, climbed up the water spout."
"F5q F5i F5q G5i A5q. A5q A5i G5q F5i G5q A5i F5q. Rq. " +
// "Down came the rain and washed the spider out."
"A5q. A5q Bb5i C6q. C6q. Bb5q A5i Bb5q C6i A5q. Rq. " +
// "Out came the sun and dried up all the rain, so the"
"F5q. F5q G5i A5q. A5q. G5q F5i G5q A5i F5q. C5q C5i " +
// "itsy, bitsy spider went up the spout again."
"F5q F5i F5q G5i A5q. A5q A5i G5q F5i G5q A5i F5q. Rq."
)
}
这里的主要课程是用于定义歌曲的记号。 这个简单的例子包括音符名称、八度音、休止符、变音和持续时间。
NOTES, OCTAVES, AND RESTS音符,八度音,休止符
音符是根据简单的A-G音阶指定的,接下来是八度音的数字。 例如,中C是C5
,高一个八度的C是C6
,而正下方的音符是B5。 这是一些乐器(如手摇铃)中常用的编号系统。
如果没有给定八度音,音符的默认八度是五度音,在中C以上。
JFugue还允许将音符指定为0到127的数字,甚至可以在音符之间定义音高(对于某些非西方音乐传统和某些类型的现代音乐),但这一高级细节不在本文的范围之内。
休止符是用R来定义的,而不是用音符-八度的组合。
ACCIDENTALS变音记号
要指定升调或降调,在音符和八度之间加一个#或b。我们的例子 "Itsy Bitsy Spider "是F大调,但JFugue默认为C大调(像标准音乐符号),所以我们需要指定第二行的B实际上是B调。稍后,我们将看到我们可以指定歌曲的调号,所以我们不需要指定调号内的升号和降号。此外,自然音可以用n来表示,以取消C大调以外的意外音(调号将在后面介绍)。不支持双升和双降。
DURATIONS持续时间
这里使用的是最简单的表达音符时长的方法,以美国系统为基础。
代码 | 持续时间 |
---|---|
W | 整个音符 |
H | 半音符 |
Q | 四分之一音符 |
I | 八分之一音符 |
S | 十六分之一音符 |
T | 三十二分之一音符 |
X | 六十四分之一音符 |
N | 一百二十八分之一音符 |
在上面的例子中,我们使用四分音符(q
)、八分音符(i
)和带点四分音符(q.
)。 在标准的音乐记谱法中,在一个音符后加一个点会使其长度延长50%。
其他时长,如三连音和其他小音符,可以用基于全音符的另一种数字符号来定义。 例如,要定义一个四分音符的C5
,用C5/0.25
。 要定义一个长度为四分之一音符的三分之一的C5
音符,就用C5/0.083333
。 我们将在后面看到,这种记谱法在将MIDI文件导入JFugue记谱法时也会用到。
播放MIDI
除了展示音乐记号外,这个例子还展示了JFugue的播放API。 具体来说,对player.play()
的调用将定义的歌曲转换为MIDI序列,并通过计算机的扬声器播放它。
ADDING MEASURES, PATTERNS, AND VOICES增加小节/模式和声部
现在,音乐符号的基本要素已经确定,我们可以开始改进这首歌。 首先,从DRY的角度来看,我们应该去掉第一行和最后一行之间的重复,它们在音乐上是相同的。 幸运的是,JFugue提供了一个使用复合设计模式的Pattern
类,允许我们重复使用音乐片段。 在下面的版本中,我们为每一个独特的线条创建一个Pattern实例,然后将它们依次添加到另一个代表整首歌的Pattern
实例中。
import org.jfugue.pattern.Pattern
import org.jfugue.player.Player
fun main(args: Array<String>) {
// "Itsy, bitsy spider, climbed up the water spout."
// and "itsy, bitsy spider went up the spout again."
val pattern1 = Pattern("F5q F5i F5q G5i | A5q. A5q A5i | G5q F5i G5q A5i | F5q. Rq. | ")
// "Down came the rain and washed the spider out."
val pattern2 = Pattern("A5q. A5q Bb5i | C6q. C6q. | Bb5q A5i Bb5q C6i | A5q. Rq. | ")
// "Out came the sun and dried up all the rain, so the"
val pattern3 = Pattern("F5q. F5q G5i | A5q. A5q. | G5q F5i G5q A5i | F5q. C5q C5i | ")
// Put the whole song together
val song = Pattern()
song.add(pattern1)
song.add(pattern2)
song.add(pattern3)
song.add(pattern1)
// Play the song
val player = Player()
player.play(song)
}
这里的另一个变化是,我们为小节(Measure
)之间的界限增加了标记("|"字符)。 有趣的是,这对程序对歌曲的解释没有任何影响—这只是为了方便用户和提高清晰度。 JFugue没有时间符号的概念,所以小节可以包含你想要的多少个拍子—它们只是为了阅读方便。
接下来,让我们使用声部(Voice
)功能来创建一个二重唱(Round
)。
import org.jfugue.pattern.Pattern
import org.jfugue.player.Player
fun main(args: Array<String>) {
// "Itsy, bitsy spider, climbed up the water spout."
// and "itsy, bitsy spider went up the spout again."
val pattern1 = Pattern("F5q F5i F5q G5i | A5q. A5q A5i | G5q F5i G5q A5i | F5q. Rq. | ")
// "Down came the rain and washed the spider out."
val pattern2 = Pattern("A5q. A5q Bb5i | C6q. C6q. | Bb5q A5i Bb5q C6i | A5q. Rq. | ")
// "Out came the sun and dried up all the rain, so the"
val pattern3 = Pattern("F5q. F5q G5i | A5q. A5q. | G5q F5i G5q A5i | F5q. C5q C5i | ")
// Put the whole song together
val song = Pattern()
song.add(pattern1)
song.add(pattern2)
song.add(pattern3)
song.add(pattern1)
val lineRest = Pattern("Rh. | Rh. | Rh. | Rh. | ")
// Create the first voice
val round1 = Pattern("V0")
round1.add(song)
// Create the second voice
val round2 = Pattern("V1")
round2.add(lineRest)
round2.add(song)
// Create the third voice
val round3 = Pattern("V2")
round3.add(lineRest, 2)
round3.add(song)
// Put the voices together
val roundSong = Pattern()
roundSong.add(round1)
roundSong.add(round2)
roundSong.add(round3)
// Play the song
val player = Player()
player.play(roundSong)
}
在这个例子中,我们通过组合三个类似的声音(类似于其他音乐背景下的音轨或通道)来创建一个重唱。 在这种情况下,每个声部都有一个单独的模式Pattern
实例。 每个模式都收到与上一个例子相同的序列,但有些模式的前缀是一整行或多整行的休止符(lineRest
),这样它们就有了交错的开始。
独立的声部是通过在模式中加入V0
, V1
和 V2
定义的。 语音声明后的所有内容都与该声部相关,直到指定另一个声部。 在这个例子中,每个声部的所有信息都集中在一起,但只要每次都重新指定声部,定义就可以穿插在一起,如下面的版本,使用前面介绍的Pattern
实例。 这个例子在音乐上与前面的例子是相同的。
import org.jfugue.pattern.Pattern
import org.jfugue.player.Player
fun main(args: Array<String>) {
// "Itsy, bitsy spider, climbed up the water spout."
// and "itsy, bitsy spider went up the spout again."
val pattern1 = Pattern("F5q F5i F5q G5i | A5q. A5q A5i | G5q F5i G5q A5i | F5q. Rq. | ")
// "Down came the rain and washed the spider out."
val pattern2 = Pattern("A5q. A5q Bb5i | C6q. C6q. | Bb5q A5i Bb5q C6i | A5q. Rq. | ")
// "Out came the sun and dried up all the rain, so the"
val pattern3 = Pattern("F5q. F5q G5i | A5q. A5q. | G5q F5i G5q A5i | F5q. C5q C5i | ")
val lineRest = Pattern("Rh. | Rh. | Rh. | Rh. | ")
// Put the whole song together
val song = Pattern()
song.add("V0 $pattern1")
song.add("V1 $lineRest")
song.add("V2 $lineRest")
song.add("V0 $pattern2")
song.add("V1 $pattern1")
song.add("V2 $lineRest")
song.add("V0 $pattern3")
song.add("V1 $pattern2")
song.add("V2 $pattern1")
song.add("V0 $pattern1")
song.add("V1 $pattern3")
song.add("V2 $pattern2")
song.add("V1 $pattern1")
song.add("V2 $pattern3")
song.add("V2 $pattern1")
// Play the song
val player = Player()
player.play(song)
}
增加CHORDS, INSTRUMENTS, KEY SIGNATURES, AND TEMPO
声部的另一个用途是添加和声或和弦伴奏。 在下面的例子中,V0
被用作前面介绍的旋律,而V1
被用来提供低音和弦。 请注意,只需要指定和弦的简称(本例中是Fmaj
和Bbmaj
,但对于更复杂的和弦还有许多其他选项),默认的八度是3号(中C以下两个八度)。 和弦也可以通过指定和弦中的每个音符来定义,用+
连接。 在这个例子中,我们对其中一个和弦使用了这种方法,以使用该和弦的第一转位,它没有一个简短的名字。
这个版本的另一个新增内容是乐器。 所有声音的默认乐器是钢琴,所以以前的例子听起来像是用钢琴演奏的。 在这个例子中,旋律是小号(指定为I[Trumpet]
或备选为I56
),和弦是教堂管风琴(指定为I[CHURCH_ORGAN]
或备选为I19
)。 这些乐器是在标题中定义的,但它们也可以在歌曲中的任何时候改变。
这里的ID号和选项来自MIDI规范;128种乐器的完整列表可在JFugue的文档中找到。
import org.jfugue.pattern.Pattern
import org.jfugue.player.Player
fun main(args: Array<String>) {
val voice1 = Pattern("V0 I[Trumpet] ")
// "Itsy, bitsy spider, climbed up the water spout."
// and "itsy, bitsy spider went up the spout again."
val pattern1 = Pattern("V0 F5q F5i F5q G5i | A5q. A5q A5i | G5q F5i G5q A5i | F5q. Rq. | ")
// "Down came the rain and washed the spider out."
val pattern2 = Pattern("V0 A5q. A5q Bb5i | C6q. C6q. | Bb5q A5i Bb5q C6i | A5q. Rq. | ")
// "Out came the sun and dried up all the rain, so the"
val pattern3 = Pattern("V0 F5q. F5q G5i | A5q. A5q. | G5q F5i G5q A5i | F5q. C5q C5i | ")
val voice2 = Pattern("V1 I[CHURCH_ORGAN] ")
//1st, 3rd, and 4th lines (third chord specified as notes)
val chord1 = Pattern("V1 Fmajh. | Fmajh. | E3h.+G3h.+C4h. | Fmajh. | ")
//2nd line
val chord2 = Pattern("V1 Fmajh. | Fmajh. | Bbmajh. | Fmajh. | ")
// Put the whole song together
val song = Pattern()
//melody
song.add(voice1)
song.add(pattern1)
song.add(pattern2)
song.add(pattern3)
song.add(pattern1)
//chords
song.add(voice2)
song.add(chord1)
song.add(chord2)
song.add(chord1, 2)
// Play the song
val player = Player()
player.play(song)
}
"Itsy Bitsy Spider "的最后一个JFugue版本在标题中增加了两个元素:调号和速度。 这些元素可以在歌曲过程中的任何地方定义,以改变调性或速度(例如,表达一个ritardano),但我们的例子只在初始阶段设置它们。 如果我们要在歌曲的过程中改变速度,我们就必须为每个声部分别改变它。
调号被指定为F大调(KFmaj
),这意味着可以从B音中去掉平声记号,就像在标准音乐记号中一样。 如前所述,默认的调号是C大调。
节奏被指定为100个 “每四分音符的脉冲”,也就是给一个四分音符多少个 “脉冲”。 JFugue的文档在速度方面实际上是相当混乱的,因为默认值是120,文档同时将其定义为每四分音符120个 "脉冲 "和每分钟120次。 这两个标度的作用方向不同,每四分音符的脉冲数越多,速度就越慢,但每分钟的节拍数越多,速度就越快。 事实上,这里使用的是第一个定义,T100
比默认速度快。
import org.jfugue.midi.MidiFileManager.savePatternToMidi
import org.jfugue.pattern.Pattern
import org.jfugue.player.Player
import java.io.File
fun main(args: Array<String>) {
val header = Pattern("KFmaj T100 V0 I[Trumpet] V1 I[CHURCH_ORGAN] ")
// "Itsy, bitsy spider, climbed up the water spout."
// and "itsy, bitsy spider went up the spout again."
val pattern1 = Pattern("V0 F5q F5i F5q G5i | A5q. A5q A5i | G5q F5i G5q A5i | F5q. Rq. | ")
// "Down came the rain and washed the spider out."
val pattern2 = Pattern("V0 A5q. A5q B5i | C6q. C6q. | B5q A5i B5q C6i | A5q. Rq. | ")
// "Out came the sun and dried up all the rain, so the"
val pattern3 = Pattern("V0 F5q. F5q G5i | A5q. A5q. | G5q F5i G5q A5i | F5q. C5q C5i | ")
//1st, 3rd, and 4th lines (third chord specified as notes)
val chord1 = Pattern("V1 Fmajh. | Fmajh. | E3h.+G3h.+C4h. | Fmajh. | ")
//2nd line
val chord2 = Pattern("V1 Fmajh. | Fmajh. | Bmajh. | Fmajh. | ")
// Put the whole song together
val song = Pattern()
song.add(header)
//melody
song.add(pattern1)
song.add(pattern2)
song.add(pattern3)
song.add(pattern1)
//chords
song.add(chord1)
song.add(chord2)
song.add(chord1, 2)
// Play the song
val player = Player()
player.play(song)
// save as a midi file for use in the next example
savePatternToMidi(song, File("spider.midi"))
}
MIDI文件的导入/存储和操作
除了像前面的例子那样将歌曲以MIDI序列的形式播放给扬声器之外,我们还可以将MIDI文件加载到JFugue库中,和/或将JFugue歌曲导出为MIDI文件。 此外,JFugue还提供了一个应用于Pattern
的音乐转换的API,库中实现了一些转换。
在下面的例子中,使用歌曲的最终版本创建的MIDI文件被加载进来,解析后的JFugue表示法被打印到命令行。 然后,用ParserListenerAdapter
将整首歌曲调高一个全音符并重新打印出来。 接下来,使用ParserListenerAdapter
将整首歌曲放慢20%,并再次重印。 最后,新修改的歌曲被导出到新的MIDI文件。
这里原作者的JFugure 4.0代码,被更改为为5.0.9对应的代码,利用Pattern.transfrom
函数来遍历歌曲的各元素,这里比较麻烦的是有两个音轨,需要分别处理。
import org.jfugue.midi.MidiDictionary.INSTRUMENT_BYTE_TO_STRING
import org.jfugue.midi.MidiFileManager.loadPatternFromMidi
import org.jfugue.midi.MidiFileManager.savePatternToMidi
import org.jfugue.parser.ParserListenerAdapter
import org.jfugue.pattern.Pattern
import org.jfugue.player.Player
import org.jfugue.theory.Note
import java.io.File
/**
* This program demonstrates MIDI I/O and musical transformations.
*/
fun main(args: Array<String>) {
val player = Player()
// load a midi file
var pattern: Pattern = loadPatternFromMidi(File("Spider.midi"))
// print the song to the console with JFugue notation
System.out.println("Original: $pattern")
// high Key, Note value increased by 2
val highKey = object : ParserListenerAdapter() {
val song: Pattern
get() =
_song.apply {
tracks.forEach {
add(it.value)
}
}
private val _song: Pattern = Pattern()
val tracks = mutableMapOf<Byte, Pattern>()
var currentTrack: Byte = 0
override fun onInstrumentParsed(instrument: Byte) {
tracks[currentTrack]?.setInstrument(instrument.toInt())
}
override fun onTempoChanged(tempoBPM: Int) {
_song.setTempo(tempoBPM)
}
override fun onTrackBeatTimeRequested(time: Double) {
tracks[currentTrack]?.add("@$time")
}
override fun onTrackChanged(track: Byte) {
currentTrack = track
tracks.getOrPut(track) {
Pattern().apply { setVoice(track.toInt()) }
}
}
override fun onNoteParsed(note: Note) {
tracks[currentTrack]?.add(Note(note.value+2, note.duration))
}
}
pattern.transform(highKey)
println("Transposed: ${highKey.song}")
// slow duration,
val slow = object : ParserListenerAdapter() {
val scale = 1.2
val song: Pattern
get() =
_song.apply {
tracks.forEach {
add(it.value)
}
}
private val _song: Pattern = Pattern()
val tracks = mutableMapOf<Byte, Pattern>()
var currentTrack: Byte = 0
override fun onInstrumentParsed(instrument: Byte) {
tracks[currentTrack]?.setInstrument(instrument.toInt())
}
override fun onTempoChanged(tempoBPM: Int) {
_song.setTempo(tempoBPM)
}
override fun onTrackBeatTimeRequested(time: Double) {
tracks[currentTrack]?.add("@${time*scale}")
}
override fun onTrackChanged(track: Byte) {
currentTrack = track
tracks.getOrPut(track) {
Pattern().apply { setVoice(track.toInt()) }
}
}
override fun onNoteParsed(note: Note) {
tracks[currentTrack]?.add(Note(note.value, note.duration*scale))
}
}
pattern.transform(slow)
println("Slowed: ${slow.song}")
// save as a midi file
savePatternToMidi(highKey.song, File("highKey.midi"))
savePatternToMidi(slow.song, File("slow.midi"))
}
提高整整一拍倒是比较简单。受限于我的乐理知识,我不清楚,把每个音符的时间长度增加20%
之后,由@1.25
指定的节拍时间是否也应该增加20%
。
程序输出:
Original: T100 V0 I56 F5q F5i F5q G5i A5q. A5q A5i G5q F5i G5q A5i F5q. Rq. A5q. A5q B5i C6q. C6q. B5q A5i B5q C6i A5q. Rq. F5q. F5q G5i A5q. A5q. G5q F5i G5q A5i F5q. C5q C5i F5q F5i F5q G5i A5q. A5q A5i G5q F5i G5q A5i F5q. V1 I19 F4h. @0.0 A4h. @0.0 C5h. F4h. @0.75 A4h. @0.75 C5h. E3h. @1.5 G3h. @1.5 C4h. F4h. @2.25 A4h. @2.25 C5h. F4h. @3.0 A4h. @3.0 C5h. F4h. @3.75 A4h. @3.75 C5h. B4h. @4.5 Eb5h. @4.5 F#5h. F4h. @5.25 A4h. @5.25 C5h. F4h. @6.0 A4h. @6.0 C5h. F4h. @6.75 A4h. @6.75 C5h. E3h. @7.5 G3h. @7.5 C4h. F4h. @8.25 A4h. @8.25 C5h. F4h. @9.0 A4h. @9.0 C5h. F4h. @9.75 A4h. @9.75 C5h. E3h. @10.5 G3h. @10.5 C4h. F4h. @11.25 A4h. @11.25 C5h.
Transposed: T100 V0 I[Trumpet] G5q G5i G5q A5i B5q. B5q B5i A5q G5i A5q B5i G5q. D0q. B5q. B5q C#6i D6q. D6q. C#6q B5i C#6q D6i B5q. D0q. G5q. G5q A5i B5q. B5q. A5q G5i A5q B5i G5q. D5q D5i G5q G5i G5q A5i B5q. B5q B5i A5q G5i A5q B5i G5q. V1 I[Church_Organ] G4h. @0.0 B4h. @0.0 D5h. G4h. @0.75 B4h. @0.75 D5h. F#3h. @1.5 A3h. @1.5 D4h. G4h. @2.25 B4h. @2.25 D5h. G4h. @3.0 B4h. @3.0 D5h. G4h. @3.75 B4h. @3.75 D5h. C#5h. @4.5 F5h. @4.5 G#5h. G4h. @5.25 B4h. @5.25 D5h. G4h. @6.0 B4h. @6.0 D5h. G4h. @6.75 B4h. @6.75 D5h. F#3h. @7.5 A3h. @7.5 D4h. G4h. @8.25 B4h. @8.25 D5h. G4h. @9.0 B4h. @9.0 D5h. G4h. @9.75 B4h. @9.75 D5h. F#3h. @10.5 A3h. @10.5 D4h. G4h. @11.25 B4h. @11.25 D5h.
Slowed: T100 V0 I[Trumpet] F5/0.3 F5/0.15 F5/0.3 G5/0.15 A5/0.44999999999999996 A5/0.3 A5/0.15 G5/0.3 F5/0.15 G5/0.3 A5/0.15 F5/0.44999999999999996 C0/0.44999999999999996 A5/0.44999999999999996 A5/0.3 B5/0.15 C6/0.44999999999999996 C6/0.44999999999999996 B5/0.3 A5/0.15 B5/0.3 C6/0.15 A5/0.44999999999999996 C0/0.44999999999999996 F5/0.44999999999999996 F5/0.3 G5/0.15 A5/0.44999999999999996 A5/0.44999999999999996 G5/0.3 F5/0.15 G5/0.3 A5/0.15 F5/0.44999999999999996 C5/0.3 C5/0.15 F5/0.3 F5/0.15 F5/0.3 G5/0.15 A5/0.44999999999999996 A5/0.3 A5/0.15 G5/0.3 F5/0.15 G5/0.3 A5/0.15 F5/0.44999999999999996 V1 I[Church_Organ] F4/0.8999999999999999 @0.0 A4/0.8999999999999999 @0.0 C5/0.8999999999999999 F4/0.8999999999999999 @0.8999999999999999 A4/0.8999999999999999 @0.8999999999999999 C5/0.8999999999999999 E3/0.8999999999999999 @1.7999999999999998 G3/0.8999999999999999 @1.7999999999999998 C4/0.8999999999999999 F4/0.8999999999999999 @2.6999999999999997 A4/0.8999999999999999 @2.6999999999999997 C5/0.8999999999999999 F4/0.8999999999999999 @3.5999999999999996 A4/0.8999999999999999 @3.5999999999999996 C5/0.8999999999999999 F4/0.8999999999999999 @4.5 A4/0.8999999999999999 @4.5 C5/0.8999999999999999 B4/0.8999999999999999 @5.3999999999999995 Eb5/0.8999999999999999 @5.3999999999999995 F#5/0.8999999999999999 F4/0.8999999999999999 @6.3 A4/0.8999999999999999 @6.3 C5/0.8999999999999999 F4/0.8999999999999999 @7.199999999999999 A4/0.8999999999999999 @7.199999999999999 C5/0.8999999999999999 F4/0.8999999999999999 @8.1 A4/0.8999999999999999 @8.1 C5/0.8999999999999999 E3/0.8999999999999999 @9.0 G3/0.8999999999999999 @9.0 C4/0.8999999999999999 F4/0.8999999999999999 @9.9 A4/0.8999999999999999 @9.9 C5/0.8999999999999999 F4/0.8999999999999999 @10.799999999999999 A4/0.8999999999999999 @10.799999999999999 C5/0.8999999999999999 F4/0.8999999999999999 @11.7 A4/0.8999999999999999 @11.7 C5/0.8999999999999999 E3/0.8999999999999999 @12.6 G3/0.8999999999999999 @12.6 C4/0.8999999999999999 F4/0.8999999999999999 @13.5 A4/0.8999999999999999 @13.5 C5/0.8999999999999999
结束语
JFugue看起还是很不错的,有空的时候来整点音乐!