资讯详情

使用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 "但是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),这样它们就有了交错的开始。

独立的声部是通过在模式中加入V0V1V2定义的。 语音声明后的所有内容都与该声部相关,直到指定另一个声部。 在这个例子中,每个声部的所有信息都集中在一起,但只要每次都重新指定声部,定义就可以穿插在一起,如下面的版本,使用前面介绍的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被用来提供低音和弦。 请注意,只需要指定和弦的简称(本例中是FmajBbmaj,但对于更复杂的和弦还有许多其他选项),默认的八度是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

标签: lance连接器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

 锐单商城 - 一站式电子元器件采购平台  

 深圳锐单电子有限公司