原文: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 "但是JFugueC大调(如标准音乐符号)是默认的,所以我们需要指定第二行B实际上是B调。稍后,我们将看到我们可以指定歌曲的调号,因此我们不需要指定调号中的升号和降号。另外,自然音可以用n来表示,以取消C大调以外的意外音(调号将在后面介绍)。不支持双升双降。
DURATIONS持续时间
| 代码 | 持续时间 |
|---|---|
| W | 整个音符 |
| H | 半音符 |
| Q | 四分之一的音符 |
| I | 八分之一的音符 |
| S | 十六分之一的音符 |
| T | 三十二分之一的音符 |
| X | 六十四分之一的音符 |
| N | 128分之一的音符 |
在上面的例子中,我们使用四分音符(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: ${
sl