65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 F# 描述音乐领域

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.09/5 (4投票s)

2024年8月12日

CPOL

4分钟阅读

viewsIcon

2406

在本文中,我们将了解如何表达用于自动生成音乐的领域。

我最近的一个项目是创建一个软件,该软件可以根据预定义的一组规则自动生成音乐。我计划引入的随机性程度将使我每次都能创造出不同的旋律,而我计划创建的规则集将确保它听起来仍然不错。 你可以在这里访问完整的源代码。下面我们将更深入地探讨它的细节。

领域描述

我计划扩展的规则是功能和声的概念。这个概念基于每个和弦都有自己功能的理念。给定和弦的功能取决于和弦“想要”去哪里,因为和声进行有两个维度:和弦的音高以及它们如何相互作用(音程层次结构);以及它在整体和声语境中的功能。因此,功能和声贯穿于创造和释放张力的循环,因此,我们会有强度不同的稳定和不稳定时刻。

三个最重要的功能是

  • 主音;可以非常稳定或感觉非常稳定,通常是乐曲或乐段的最后一个和弦
  • 下属音;准备和声终止,并引入一定程度的不稳定性
  • 属音;最不稳定的和弦,希望解析为另一个和弦

编码领域

现在让我们将这些知识编码到代码中。我选择了 F# 来完成这项任务,因为它的类型系统非常适合表达各种领域。

让我们从基础开始,描述我们调色板中的和弦。

type ChordQuality =
    | Major
    | Minor

还有很多其他的和弦性质,但这对于我们的需求来说已经足够了。

现在,让我们描述一下我们从上一段获得的知识。

type HarmonyItem =
    | Tonic
    | SubDominant
    | Dominant

它们之间的过渡将如下所示

type HarmonyTransition =
    | Dublicate
    | IncreaseTension
    | MaximizeTension
    | DecreaseTension
    | Resolve

现在让我们看看如何应用过渡

let applyCommand command chord =
    match command with
    | Dublicate -> dublicate chord
    | IncreaseTension -> increaseTension chord
    | DecreaseTension -> decreaseTension chord
    | MaximizeTension -> maximizeTension chord
    | Resolve -> resolve chord

let dublicate harmonyItem =
    harmonyItem

let increaseTension harmonyItem =
    match harmonyItem with
    | Tonic -> SubDominant
    | SubDominant -> Dominant
    | Dominant -> Dominant

let decreaseTension harmonyItem =
    match harmonyItem with
    | Tonic -> Tonic
    | SubDominant -> Tonic
    | Dominant -> SubDominant

let maximizeTension harmonyItem =
    Dominant

let resolve harmonyItem =
    Tonic

话虽如此,让我们来看看我们的功能进行中每个项目背后隐藏的东西。基本上,每个和弦都会有一个质量,并且它与根音之间有一定的音符偏移。

type HarmonyItemValue = {
    value: int
    chordQuality: ChordQuality
}

let getHarmonyItemValue item =
    match item with
    | Tonic -> { value = 0; chordQuality = Major }
    | SubDominant -> { value = 5; chordQuality = Major }
    | Dominant -> { value = 7; chordQuality = Major }

鉴于此,我们可以从每个和声项目中创建一个音高数组。

type Pitch = {
    midiNote: int
    duration: float
}

let createChordFromRootNote rootNote item =
    let itemValue = getHarmonyItemValue item
    match (itemValue.value, itemValue.chordQuality) with
    | (value, Major) -> [|
        {
            midiNote = rootNote + value
            duration = 1.0
        };
        {
            midiNote = rootNote + value + 4
            duration = 0.125
        };
        {
            midiNote = rootNote + value + 7
            duration = 1.0
        }|]
    | (value, Minor) -> [|
        {
            midiNote = rootNote + value
            duration = 1.0
        };
        {
            midiNote = rootNote + value + 4
            duration = 0.125
        };
        {
            midiNote = rootNote + value + 7
            duration = 1.0
        }|]

生成进行

因此,为了每次创建不同的进行,我们需要向过程中添加一些随机性。 为了实现这一点,我们将为每个过渡关联一个概率。 假设我们在主音和弦中,并且我们有 0.1 的概率在下一个和弦中保持在那里,而增加张力的概率彼此相等并且总计各为 0.45。 在这种情况下,让我们为每个过渡分配一个阈值。 假设主音是 0.1,下属音是 0.55,这是 0.1 的主音阈值 + 主音概率,属音是 1.0,这是完整事件组的概率。 在这种情况下,一旦我们生成一个介于 0.0 和 1.0 之间的随机数,我们就可以选择阈值大于给定随机数的最小项目。

这是它在代码中的样子。

type HarmonyTransitionProbability = {
    transition: HarmonyTransition
    coinThreshold: float
}

let regenerateHarmonyTransitionProbability currentHarmonyItem =
    match currentHarmonyItem with
    | Tonic ->
        [|
            { transition = Dublicate; coinThreshold = 0.1 };
            { transition = IncreaseTension; coinThreshold = 0.55 };
            { transition = MaximizeTension; coinThreshold = 1.0 };
        |]
    | SubDominant ->
        [|
            { transition = Dublicate; coinThreshold = 0.1 };
            { transition = IncreaseTension; coinThreshold = 0.55 };
            { transition = Resolve; coinThreshold = 1.0 };
        |]
    | Dominant ->
        [|
            { transition = Dublicate; coinThreshold = 0.1 };
            { transition = Resolve; coinThreshold = 0.9 };
            { transition = DecreaseTension; coinThreshold = 1.0 };
        |]

let rnd = Random()

let generateNextChord currentChord coin =
    let probabilityMap = regenerateHarmonyTransitionProbability currentChord
    let command = (Array.filter (fun x -> coin <= x.coinThreshold) probabilityMap).[0].transition
    applyCommand command currentChord

let generateProgression (initialChord: HarmonyItem) (length: int) : HarmonyItem array =
    let rec generate (currentChord: HarmonyItem) (remaining: int) (progression: HarmonyItem list) =
        if remaining = 0 then
            List.toArray (List.rev progression)
        else
            let coin = rnd.NextDouble()
            Console.WriteLine(coin)
            let nextChord = generateNextChord currentChord coin
            generate nextChord (remaining - 1) (nextChord :: progression)
    generate initialChord (length - 1) [initialChord]

领域演变

到目前为止,我们只介绍了几个基本概念。但即使是一些更主流的进行,例如Axis of Awesome 4 和弦 wamp也基于替代的概念。 替代是我们已经知道的和声功能的副本,但不如它们的对应物那样鲜明。 因此,让我们也将它们引入我们的领域。

对我来说,这是用 F# 表达我的领域最愉快的部分,因为我必须记住在两个地方添加它:和声项目以及它们之间的过渡。

type HarmonyItem =
    | Tonic
    | TonicSubstitute1
    | TonicSubstitute2
    | SubDominant
    | Dominant

type HarmonyTransition =
    | Dublicate
    | IncreaseTension
    | MaximizeTension
    | DecreaseTension
    | DecreaseTensionToFisrtSubstitute
    | DecreaseTensionToSecondSubstitute
    | Resolve
    | ResolveToFirstSubstitute
    | ResolveToSecondSubstitute

此时,在我应用模式匹配的任何地方,编译器都会向我发出关于不完整模式匹配的警告。 所以我只是添加缺失的情况,直到编译器满意为止,瞧:一个新版本的域就完成了。 在某种程度上,这让我想起了来自永恒经典“有效地处理遗留代码”的依赖于编译器的技术。

生成声音

此时,我们可以生成 MIDI 音符的音高数组。 为了从这些音符创建声音,我使用了一种名为 SuperCollider 的专用编程语言。 我不会在这里详细介绍,但如果您有兴趣,可以看一下代码。 请注意,那里有很多分支,所有分支都包含一些有趣的代码。

结论

我一直是 F# 的拥护者很长时间了。 因此,我不会再次扩展其类型系统的强大功能,而只是在此处留下一个链接到我最喜欢的曲目之一,该曲目是使用本文中的代码创建的。

© . All rights reserved.