用 F# 描述音乐领域

1 篇文章 0 订阅

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

一.域描述

我计划扩展的规则是功能和声的概念。这个概念基于每个和弦都有其自身功能的想法。给定和弦的功能取决于和弦“想要”接下来去哪里,因为和声进行有两个维度:和弦的音高及其相互作用方式(音程层次);以及它在整个和声环境中的功能。因此,功能和声经历了产生和释放张力的循环,因此,我们有稳定和不稳定的时刻,它们的强度各不相同。

最重要的三个功能是,

  • 主音:可以是或感觉非常稳定,通常是一段乐曲或一段乐段的最后一个和弦
  • 次属音:准备和声节奏并引入一定程度的不稳定性
  • 主导音:是最不稳定的和弦,想要解决另一个和弦

二.对域名进行编码

现在让我们将这些知识编码成代码。我选择 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 chord wamp也基于替代的概念。替代是我们已经知道的谐波函数的副本,但不如其对应项那么明显。所以让我们也在我们的领域中介绍它们。

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

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

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

此时,在我应用模式匹配的任何地方,编译器都会向我发出有关模式匹配不完整的警告。因此,我只需添加缺失的案例,直到编译器满意为止,然后瞧:域的新版本就完成了。在某种程度上,这让我想起了经典之作“有效处理遗留代码”中的精益编译器技术。

五.产生声音

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

六.结论

我已经支持 F# 很长时间了。因此,我不会再一次阐述其类型系统的强大功能,而是在这里留下一个我最喜欢的曲目的链接,该曲目是用本文中的代码创建的。

  • 15
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谢.锋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值