这本书根据第一原理
讨论了数字音乐的创作和实现, 当然顺便教授了Haskell. 不过, 与标题不符的是, 这本书的内容肯定并不局限于Haskell.
许多计算机科学家和数学家都对音乐怀有浓厚的兴趣, 而且似乎在其中一门学科中具有强烈亲和力或敏锐感受力的人, 往往在另一门学科中也同样出色. 因此, 自然会去思考这两者是如何相互作用的. 事实上, 音乐与数学之间的互动有着悠久的历史, 可以追溯到古希腊人基于算术关系构建音阶, 也包括许多古典作曲家对数学结构的运用, 对音乐进行形式化的和声分析, 以及许多现代音乐创作技术. 高级音乐理论广泛借鉴了数学的不同分支中的思想, 例如数论, 抽象代数, 拓扑学, 范畴论, 微积分等等.
将计算机与音乐相结合的努力同样有着悠久的历史. 当今的大多数消费类电子产品都是数字化的, 音频处理和录音的大多数形式也都是如此. 除此之外, 数字乐器提供了新的表现方式, 记谱软件和音序器已经成为职业音乐人的标准工具, 而那些最具计算机科学素养的人则利用计算机来探索新的作曲, 变换, 演奏和分析方式.
本教材采用一种以编程语言为中心的方法来探讨计算机音乐的基础. 具体而言, 使用函数式编程语言Haskell来表达所有的计算机音乐概念. 因此, 在学习计算机音乐概念的同时, 也会顺带学习如何使用Haskell进行编程. 核心的音乐思想被整理进一个名为Euterpea的Haskell库中. Euterpea
这一名称源自Euterpe, 她是古希腊九位缪斯女神之一, 也是专司音乐的女神.
在过去几十年里, 计算机音乐领域天文数字般增长, 其内容可以沿着多个维度进行结构化和组织. 对于编程语言而言, 一个特别有用的维度是将高层次的音乐问题与低层次的音乐问题区分开来. 由于使用了高层次
编程语言——也就是Haskell——在这两个音乐层次上进行编程, 为了避免混淆, 本书将在音乐维度中使用音符层次和信号层次这两个术语.
在音符层次, 被考虑的最基本音乐实体是一个音符 (即音高和时长), 其余一切都是在此基础上构建的. 在这个层次上, 除了音乐的传统表示方法之外, 我们还可以研究所谓算法作曲的一些有趣方面, 包括分形, 基于文法的系统, 随机过程等等的运用. 在此基础上, 我们也可以研究音乐的和声与节奏分析, 尽管本教材当前并未以此为重点. Haskell通过其强大的数据抽象机能, 高阶函数以及声明式语义, 极大地便利了在这一层次上的编程.
相比之下, 在信号层次, 关注点是计算机音乐应用中实际生成的声音, 因此信号是被考虑的最基本实体. 在数字计算机中, 声音通常通过对连续音频信号进行离散采样来具体表示, 采样率足够高以致于人耳无法区分离散与连续, 通常为每秒个采样 (这是CD的标准采样率). 但在Euterpea中, 这些细节被隐藏了: 信号被抽象地视作连续量. 这极大地降低了使用离散值序列进行编程的负担. 在信号层次, 我们可以研究声音合成技术 (例如模拟传统乐器的声音, 或创造完全人工的声音), 音频处理 (例如确定信号的频谱), 以及特殊效果 (混响, 声像, 失真等).
假设一位音乐家使用节拍器演奏音乐, 节拍器设定为,也就是每分钟拍. 这意味着一拍需要秒. 在每秒采样率为的立体声系统中, 这又被转换为个采样点, 而每个采样点通常占用计算机内存的几个字节.
相比之下, 在音符层次, 我们只需要某种运算符或数据结构来表示演奏这个音符
, 这总共只需要很少一些的字节. 这种戏剧性的差异突出了在音符层次编程与在信号层次编程之间的关键计算性区别之一.
当然, 许多计算机音乐应用同时涉及音符层次和信号层次, 而且确实需要有一种机制来在两者之间进行协调. 虽然这种协调可以有多种形式, 但在大多数情况下都是直接的, 这也是音符层次和信号层次区分起来如此自然的另一个原因.
本教材首先探讨音符层次 (第1–17章), 随后考察信号层次 (第18–23章). 如果你只对信号层次感兴趣, 你可能希望跳过第9–17章.
编程, 从最广义上讲, 就是问题解决. 它始于识别那些可以并且应该用数字计算机解决的问题. 因此, 编程的第一步是回答这个问题: 我正在尝试解决什么问题?
一旦问题被理解, 就必须找到一个解决方案. 当然, 这可能并不容易, 而且你可能会发现多个解决方案, 因此需要有一种度量成功的方法. 度量的维度有很多, 包括正确性 (我能得到正确的答案吗?
) 和效率 (它运行得够快吗, 或者会不会占用太多内存?
). 但是, 判断哪一个解决方案更好并不总是清晰的, 因为维度可能很多, 于是程序往往在某一维度上表现优秀, 而在其他维度上表现不佳. 例如, 可能存在某个解决方案是最快的, 某个解决方案使用内存最少, 某个解决方案最容易理解. 决定选择哪一个可能很困难, 这也是编程中较有趣的挑战之一.
前面提到的最后一个成功衡量标准——程序的清晰度——有些难以捉摸, 很难量化和度量. 然而, 在大型软件系统中, 清晰度是一个特别重要的目标, 因为这样的系统通常由多人长期维护, 并且随着系统的发展会发生很大变化. 易于理解的代码使得修改变得容易得多.
在计算机音乐领域, 清晰度的重要性还有另一个原因: 即代码往往反映了作者的思路, 音乐意图和艺术选择. 传统的乐谱通常无法表达作曲者在创作音乐时的思考过程, 但程序往往可以. 所以, 当你编写程序时, 要写给别人看, 并追求优雅和美感, 就像你对待最终的音乐作品一样.
编程本身就是一个创造性的过程. 有时候, 编程的解决方案 (或艺术创作) 会突然涌现, 几乎不费力. 然而, 更常见的情况是, 它们只有在经过大量努力之后才会被发现! 我们可能会编写一个程序, 对其进行修改, 丢掉重新开始, 放弃, 再重新开始, 等等. 重要的是要意识到, 这种辛苦的反复修改程序是常态, 事实上, 你应该养成这种习惯. 不要总是满足于你的第一个解决方案, 并且始终准备回去修改, 甚至丢弃那些你不满意的程序部分.
在学习一门新的编程语言时, 充分理解该语言中的程序是如何被执行的是非常有帮助的——换句话说, 就是理解一个程序意味着什么
. 对Haskell程序的执行过程, 或许可以最好地理解为一种通过计算来进行的求值过程. Haskell中的程序可以被看作是函数, 这些函数的输入对应于所要解决的问题, 而它们的输出则是期望得到的结果——而函数的行为可以有效地理解为是通过计算来完成的.
一个涉及数字的例子或许有助于说明这些思想. 数字在许多应用中都会被用到, 计算机音乐也不例外. 例如, 整数可以用来表示音高, 而浮点数则可以用来进行与频率或振幅相关的计算.
假设我们想要进行算术计算, 例如. 在Haskell之中, 这将写作3 * (9 + 5), 因为大多数标准计算机键盘和文本编辑器不支持特别的符号. 结果可以按照如下方式进行计算:
前几章介绍了Haskell函数式编程的一些基本概念. 同时, 也介绍了Euterpea的若干函数和运算符, 例如note, rest, (:+:), (:=:)和trans. 本章将揭示这些函数和运算符的实际定义, 从而展示Euterpea在音符层面的底层结构和整体设计. 此外, 还将引入许多其他音乐概念, 并在此过程中介绍更多Haskell特性.
有时使用Haskell内置的数据类型直接表示某些令人感兴趣的概念会很方便. 例如, 我们可能希望使用Int来表示八度, 其中按照约定, 第4个八度对应钢琴上的中央C所在的八度. 我们可以在Haskell中使用类型别名(type synonym)来表达这一点:
type Octave = Int类型别名并不会创建新的数据类型, 它只是给已有类型取了一个新名字. 类型别名不仅可以为原子类型 (如Int) 定义, 也可以为结构类型 (如元组) 定义. 例如, 正如上一章讨论的, 在音乐理论中, 音高 (pitch) 被定义为由音级(pitch class)和八度构成的序对. {译注: 音级, 或者说音高类, 的确可以视为音高的等价类.} 假设存在一个名为PitchClass的数据类型 (我们稍后会回到这个话题), 我们可以写出如下类型别名:type Pitch = (PitchClass, Octave)例如, concert A (即A440) 对应于音高(A, 4) :: Pitch, 而钢琴上最低和最高的音分别对应于(A, 0) :: Pitch和(C, 8) :: Pitch.另一个重要的音乐概念是时长(duration). Euterpea没有使用整数或者浮点数, 而是使用有理数来表示时长:
type Dur = RationalRational是有理数的数据类型, 其在Haskell中被表达为Integer的比. 选择使用Rational有些主观性, 但是这种选择可以由三个观察澄清: (1) 音乐理论里的许多时长是以比例表达的 (5:4拍, 四分之一音符, 附点音符, 等等). (2) 有理数是精确的 (这和浮点数不同), 这在许多计算机音乐应用中是重要的. (3) 我们很少需要用到无理数.由GHC所输出的Haskell的有理数的形式为n % d, 其中n是分子而d是分母.
到目前为止一切都好. 不过, 何为PitchClass? 或许我们想要使用整数来表示音级, 但是这并不是十分优雅. 理想情况下, 我们想要写下和传统音级名称类似的东西, 诸如等等. 我们的解决方案是使用Haskell中的代数数据类型:
data PitchClass = Cff | Cf | C | Dff | Cs | Df | Css | D | Eff | Ds
| Ef | Fff | Dss | E | Ff | Es | F | Gff | Ess | Fs
| Gf | Fss | G | Aff | Gs | Af | Gss | A | Bff | As
| Bf | Ass | B | Bs | BssPitchClass数据类型本质上就是枚举了35个音级名称 (从A到G每个音符名称各五个). 注意到双升和双降都有囊括在内, 这导致了许多同音异名 (enharmonics), 即两个音符听起来相同
, 例如和.
(音级的排列顺序可能看起来有点奇怪, 但是这里的想法在于如果一个音级pc1在另一个音级pc2的左边, 那么pc1的音高要低于
pc2. 这个想法将会在第7.1节得到形式化和讨论.)
请记得PitchClass是一个全新的用户定义的数据类型, 它和其他任何类型都不相等. 这也就是data声明区别于type声明的地方. 作为另一个使用data声明的例子, Haskell的布尔数据类型, 叫做Bool, 其在Haskell之中被简单地预定义为:
data Bool = False | True我们当然可以出于其他目的定义其他的数据类型. 例如, 我们想要定义音符(note)和休止(rest)的概念. 这些都可以被想成是原始 (primitive)
的音乐值, 因此作为初次尝试我们写下
data Primitive = Note Dur Pitch
| Rest Dur类比于之前的数据类型声明, 以上的声明是说一个Primitive要么是一个Note, 要么是一个Rest. 然而, 有所不同的是, 构造子Note和Rest接受参数, 就和函数一样. 对于Note而言, 其需要两个参数, 类型分别为Dur和Pitch, 而Rest需要一个参数, 其类型为Dur. 换言之, Note和Rest的类型分别为:Note :: Dur -> Pitch -> Primitive
Rest :: Dur -> Primitive例如Note qn a440是作为四分之一音符演奏的concert A, 而Rest 1是一个全音符休止.然而, 这个定义并不全然令人满意, 因为我们想要给音符附加其他的信息, 例如其响度, 或者其他的注记和articulation. 而且, 音高本身可能实际上是一个打击乐声音, 其压根就没有真正的音高. 为了解决这个问题, Euterpea使用了Haskell中的一个重要概念, 即多态性(polymorphism)——这是对于类型进行参数化或者抽象的能力. (poly表示多, morphism指的是对象的结构, 或者说形式.)
Primitive可以按照以下方式被重新定义为一个多态数据类型. 我们没有固定音符的音高的类型, 而是将其留待为未作刻画, 通过类型变量的使用:
data Primitive a = Note Dur a
| Rest Dur注意到类型变量a被用作Primitive的一个参数, 然后其用在了声明的体里, 就像函数中的变量一样. 这个版本的Primitive比之前的版本更为一般——诚然如此, 注意到Primitive Pitch和之前版本的Primitive是一样的 (或者, 更技术性地说, 同构于). 但是, Primitive现在比之前的版本更为灵活, 例如我们可以给音高添加响度信息, 即Primitive (Pitch, Loudness). 其他关于这个想法的具体实例将会在之后引入.
另一种解释这个数据声明的方法是言称对于任意的类型a, 这个声明声明了其构造子的类型为:
Note :: Dur -> a -> Primitive a
Rest :: Dur -> Primitive a即便Note和Rest被称为数据构造子, 它们仍然是函数并且它们具有类型. 因为它们的类型签名中都具有类型变量, 所以它们都是多态函数的例子.将多态性视为在类型层次应用抽象原则是很有帮助的——事实上, 它通常被称为类型抽象. 第3章将详细探讨更多多态函数和多态数据类型的例子.
到目前为止, 我们已经引入了Euterpea原始的音符和休止, 但是如何将许多音符和休止组合成为更大的乐曲呢? 为了实现这一点, Euterpea定义了另一个多态数据类型, 这可能是本教材中使用的最重要的数据类型, 它定义了音符层次的音乐实体的基本结构:
data Music a =
Prim (Primitive a) -- primitive value
| Music a :+: Music a -- sequential composition
| Music a :=: Music a -- parallel composition
| Modify Control (Music a) -- modifier根据之前的推理, 这些构造子的类型为:Prim :: Primitive a -> Music a
(:+:) :: Music a -> Music a -> Music a
(:=:) :: Music a -> Music a -> Music a
Modify :: Control -> Music a -> Music a这四个构造子自然也是多态函数.Music数据类型声明基本上是说Music类型的值具有四种可能的形式:
Prim p是一个终结结点, 其中p是一个类型为Primitive a的原始值, 对于某个类型a而言, 例如:a440m :: Music Pitch
a440m = Prim (Note qn a440)是与concert A的四分之一音符演绎对应的音乐值.m1 :+: m2是m1和m2的顺序复合; 即m1和m2按次序演奏.m1 :=: m2是m1和m2的并行复合; 即m1和m2同时演奏. 作为结果的时长取决于m1和m2中更长的那个.Modify cntrl m是m的一个注解版本, 其中控制参数
cntrl刻画了修饰m的某种方式.将这些音乐想法表示为递归数据类型是很方便的, 因为它不仅允许我们构造音乐值, 还可以拆解它们, 分析它们的结构, 以保持结构的方式打印它们, 转换它们, 为演奏目的解释它们, 等等. 本教材将展示许多这类处理过程的例子.
Control数据类型被Modify构造子用来为Music值添加注释, 包括节奏改变, 移调, 乐句属性, 乐器, 调号或自定义标签. 这个数据类型目前并不重要, 但为了完整性, 这里给出它的完整定义:
data Control =
Tempo Rational -- scale the tempo
| Transpose AbsPitch -- transposition
| Instrument InstrumentName -- instrument label
| Phrase [PhraseAttribute] -- phrase attributes
| KeySig PitchClass Mode -- key signature and mode
| Custom String -- custom label
data Mode = Major | Minor | Ionian | Dorian | Phrygian
| Lydian | Mixolydian | Aeolian | Locrian
| CustomMode StringAbsPitch只是Int的类型别名 (绝对音高
, 第2.4节将会定义). 乐器名是借用自General MIDI标准 [3, 4] 的, 并被捕获为图2.1中的代数数据类型. KeySig构造子给一个Music值附加一个调号, 其与移调是不同的. 对于乐句属性和自定义标签的完整解释要推迟到第9章.
出于方便的考量, Euterpea定义了很大函数用于编写特定种类的音乐值. 作为开始:
note :: Dur -> a -> Music a
note d p = Prim (Note d p)
rest :: Dur -> Music a
rest d = Prim (Rest d)
tempo :: Dur -> Music a -> Music a
tempo r m = Modify (Tempo r) m
transpose :: AbsPitch -> Music a -> Music a
transpose i m = Modify (Transpose i) m
instrument :: InstrumentName -> Music a -> Music a
instrument i m = Modify (Instrument i) m
phrase :: [PhraseAttribute] -> Music a -> Music a
phrase pa m = Modify (Phrase pa) m
keysig :: PitchClass -> Mode -> Music a -> Music a
keysig pc mo m = Modify (KeySig pc mo) m注意到这些函数每个都是多态的, 这个特性继承自其所使用的数据类型. 另外回忆一下, 这些函数中的前两个在前一章的例子里也有用到.我们还可以为熟悉的音符, 时值和休止创建简单的名称, 如图2.2和图2.3所示. 尽管它们数量很多, 但这些名称足够不同寻常
, 因此不太可能发生名称冲突.
将音高简单视为整数在许多情况下是很有用的, 所以说Euterpea使用了一个类型别名定义了绝对音高
的概念:
type AbsPitch = Int一个(相对)音高的绝对音高可以被数学地定义为倍的八度数然后加上音级的索引数. 我们可以在Haskell中将其表达为如下代码:absPitch :: Pitch -> AbsPitch
absPitch (pc, oct) = 12 * (oct + 1) + pcToInt pcpcToInt是一个函数, 其将一个特定的音级转换为一个索引数, 它的表达是容易的但也是乏味的, 如图2.4所示. 不过, 这里存在一个微妙的地方: 根据音乐理论的传统, 音高是被分配一个范围从到的整数, 即模, 从音级C开始. 换言之, C的索引是, 的索引是, 的索引是. 然而, 这意味着(C, 4)的绝对音高是, 而(Cf, 4)的绝对音高会是. 后者似乎看起来不太正确; 是更理性的选择. 因此, pcToInt的定义的写作方式注意了避免边界的问题, 也就是说到之外的数字也有使用. 以这个定义, absPitch (Cf, 4)将会产生59, 这是我们所期望的.
将一个绝对音高转换为一个音高更加微妙, 鉴于同音异名的存在. 例如, 绝对音高既可以对应于(Ds, 1)又可以对应于(Ef, 1). {译注: 译者觉得这里存在笔误, 实际上根据定义应该是(Ds, 0)和(Ef, 0).} Euterpea的方法是总是在这种模棱两可的情况下返回具有一个升调的音级:
pitch :: AbsPitch -> Pitch
pitch ap =
let (oct, n) = divMod ap 12
in ([C, Cs, D, Ds, E, F, Fs, G, Gs, A, As, B] !! n, oct - 1)给定pitch和absPitch, 现在很容易定义一个函数trans, 其对于音高进行transpose (移调?):trans :: Int -> Pitch -> Pitch
trans i p = pitch (absPitch p + i)到目前为止, 前一章所引入的运算符和函数都已覆盖.在前几章中已经介绍了若干多态数据类型的例子. 本章将重点放在多态函数上, 而多态函数通常定义在多态数据类型之上.
大家已经很熟悉的列表是多态数据类型的典型例子, 本章将对其进行深入研究. 尽管列表与音乐并没有直接的联系, 但它们可能是Haskell中最常用的数据类型, 并且在计算机音乐编程中有着广泛的应用. 此外, Music数据类型本身也是多态的, 本章还将定义若干以多态方式作用于它的新函数.
(关于作用于列表的预定义多态函数的更详细讨论, 可参见附录A.)
本章还将介绍高阶函数, 即以一个或多个函数作为参数, 或者返回一个函数作为结果的函数 (函数本身也可以被放入数据结构中). 高阶函数使得许多音乐概念能够以优雅而简洁的方式表达. 与多态性结合起来, 高阶函数极大地增强了程序员的表达能力以及代码复用能力.
这两个新思想都是在前面已经建立的基础之上自然发展而来的.
在前面的章节中,我们介绍了包含多种不同类型元素的列表示例——整数, 字符, 音级等等——我们完全可以想象需要使用其他元素类型的列表的情况. 然而, 有时候我们并不需要对元素的类型如此特定. 例如, 假设我们想定义一个函数length来确定列表中元素的个数. 列表是包含整数, 音级, 还是甚至包含其他列表, 这其实并不重要——我们可以想象在每种情况下都以完全相同的方式计算长度. 显而易见的定义是:
length [] = 0
length (x : xs) = 1 + length xs这个递归定义是自明的. 诚然如此, 我们可以这样阅读这些等式: 空列表的长度为0, 第一个元素是x而余下的列表是xs的列表的长度是1再加上xs的长度.
但是length的类型应该是什么呢?
到目前为止, 关于Haskell和Euterpea的细节已经介绍得足够多了, 因此值得建立一两个小而完整的应用. 在本章中, 我们将首先考察如何将已有的音乐转写为Euterpea, 从而展示如何在Euterpea中表达传统的音乐思想. 我们还将介绍Haskell的模块系统, 这对于构建较大型的项目非常重要. 最后, 将呈现一种简单形式的算法作曲, 在这个过程中可以清楚地看到, 除了简单结构之外, 更为奇特的内容也能够被轻松地表达出来.
让我们从一些简单且在音乐上可能很熟悉的内容开始: Twinkle Twinkle Little Star
的旋律, 其乐谱如图4.1所示. 这是一段相当直接, 容易转写的旋律. 其中一种做法是, 直接按照乐谱上的样子, 使用Euterpea提供的用于创建Note的简写形式, 将所有音符逐一写出来.
twinkle =太过冗长了! 如果我们试图转写一首更复杂的音乐作品, 这个过程将会极其无聊. 好在我们可以做得更好.
首先注意到, 这是一个纯粹的顺序模式, 因此非常适合使用line来将一组音符连接起来. 不过, 单靠这一点并不能为我们节省太多工作, 得到的定义仍然会包含一个非常长的音符列表. 另外还有两个值得注意的地方: 重复出现的副歌段落 (这首歌整体上具有 ABA 的结构), 以及大量出现的且都位于同一八度内的四分音符. 现在, 让我们将这些观察与前面章节中介绍的一些语言特性结合起来, 构造一个更加简洁的定义.
本章介绍了更多的一些Haskell的句法设备, 其可以用来编写精确而直觉性的程序. 这些设备将在本书的剩余部分经常用到.
第3章引入了currying的使用作为简化程序的方式. 这个句法设备依赖于通常函数应用的方式以及这些应用如何被parse.
本章探索了一些操作Euterpea音乐结构的简单策略. 这里描述的操作反映了音乐创作中用于转换音乐素材的常见手法, 例如旋律倒影(或者说反转)的概念.
我们可以对于一个音乐值的起始进行时间上的偏移(offset), 也就是在其前面插入休止, 这个概念可以打包为以下函数:
offset :: Dur -> Music a -> Music a
offset d m = rest d :+: m使用offset, 很容易编写卡农式的结构, 例如m :=: offset d m, a song written in rounds (见练习3.14), 等等.回忆一下第4章里函数times将一段音乐重复特定次数:
times :: Int -> Music a -> Music a
times 0 m = rest 0
times n m = m :+: times (n - 1) m更为有趣的是, Haskell的非严格语义允许我们定义无限的音乐值. 例如, 一个音乐值可以永久(forever)重复下去, 使用下列简单的函数:
forever :: Music a -> Music a
forever m = m :+: forever m因此, 例如, 一个无限循环的固定音型 (ostinato) 可以用这种方式表达, 然后在不同的上下文中使用, 这些上下文会自动提取实际需要的部分. 创建这类上下文的函数将在稍后描述.倒影, 逆行, 倒影逆行等在12音理论中所使用的概念很容易在Euterpea中捕获. 这些术语通常只是应用于一行 (line)
音符的, 即一段旋律 (在12音理论中, 其被称为一个行 (row)
). 一行的逆行只不过是逆转, 即以相反顺序演奏音符. 一行的倒影是相对于某个给定音高的 (根据约定, 通常是第一个音高), 其中相继音高之间的音程被反转, 即取负. 如果第一个音符的绝对音高是ap, 那么每个音高p都会被转换为绝对音高ap - (absPitch p - ap), 换言之即2 * ap - absPitch p.
MIDI是乐器数字接口
的缩写, 也是控制电子乐器的标准协议. 本章描述了Euterpea用以将Music值转换为MIDI文件和流的过程.
MIDI是绝大多数电子乐器和个人电脑制造商所采用的标准. 其核心是一个沟通音乐事件 (例如音符起 (note on)
和音符止 (note off)
) 所谓的元事件 (选择合成器补丁 (synthesizer patch), 改变节奏, 等等) 的协议. 除了逻辑协议, MIDI标准也描述了电子信号特征和布线细节 (cabling detail), 以及一个标准MIDI文件.
MIDI文件和MIDI流只处理抽象音乐信息, 这就和Euterpea的Music值差不多. 信息的存在性考虑的是在诸多乐器上合适开始和停止音符 (pitch), 但是对于这些乐器的实际实现则服从于解释. MIDI并不包含你所实际听到的声音的信息, 因而在不同计算机上甚至在相同计算机的不同软件中播放相同的MIDI文件将会产生不同的声音. 负责将MIDI数据转变为声音的软件和硬件被称为合成器. 也存在硬件MIDI控制器, 其是用于将MIDI数据传送至电脑或者硬件合成器的用户界面. 一些MIDI设备拥有这些功能的组合.
General MIDI是一个标准,
到目前为止, 我们对于Haskell中的音乐值的呈现基本上是结构性的, 即句法性的. 尽管我们已经给过了一个对于Music值的时长的解释 (如在dur, cut, remove等中所呈现的那样), 我们还没有给出任何更为深刻的音乐解释. 这些音乐值实际意味着什么, 即其语义或者解释是什么? 对于句法构造给出语义解释的形式过程在计算机科学中非常常见, 特别是在编程语言理论之中. 不过, 其在音乐中也非常常见: 对于音乐的解释是音乐演出的本质. 然而, 在传统音乐之中这个过程通常是非形式化的, 诉诸于美学判断和美学价值观. 我们要做的事情是使得这个过程形式化, 但仍然保持灵活性, 以允许不止一个可能的解释, 这和人类对于音乐的演出是一样的.
首先我们需要言称何谓抽象演出. 我们的方法在于将演出考虑为按时间排序的音乐事件序列, 其中每个事件都捕获了单独一个音符的演奏. 回忆一下下列第8章的定义:
data MEvent = MEvent {
eTime :: PTime, -- onset time
eInst :: InstrumentName, -- assigned instrument
ePitch :: AbsPitch, -- pitch number from 0-127
eDur :: DurT, -- note duration
eVol :: Volume, -- volume from 0-127
eParams :: [Double]} -- optional other parameters
deriving (Show,Eq,Ord)
type Performance = [MEvent]
type PTime = Rational
type DurT = Rational一个事件MEvent {eTime = s, eInst = i, ePitch = p, eDur = d, eVol = v}捕获了这样的事实, 在起始时间s, 乐器i奏响了音符p, 音量为v, 持续时长为d (其中持续时长的度量为秒而非拍子). (一个事件的ePararms是为了除了MIDI之外的乐器设计的, 特别是我们使用第19章所描述的技术设计的乐器.)
为了给音乐值生成完整的演出或者赋予解释, 我们必须要知道何时开始演出, 合适的乐器, 音量, 起始音高偏移量, 以及节奏. 我们可以将其想成是音乐值解释的上下文
. 这种上下文在Haskell中可以形式化地被捕获为一个数据类型:
data Context a = Context {cTime :: PTime,
cPlayer :: Player a,
cInst :: InstrumentName,
cDur :: DurT,
cPch :: AbsPitch,
cVol :: Volume,
cKey :: (PitchClass,Mode)}
deriving Show当一个Music值被解释的时候, 其会被赋予一个初始的上下文. 但是, 随着Music被递归地解释, 上下文会进行更新以反映诸如节奏变更, 移调等事件. 这很快会得到清晰说明.上下文的DurT分量是一个全音符以秒计的时长. {译注: DurT疑似应该为cDur.} 为了使得计算更加容易, 我们可以定义一个节拍器 (metronome)
函数, 其接受一个标准的节拍器标记 (以拍/分钟计) 和与一拍相对应的音符类型 (四分之一音符, 八分之一音符, 等等), 生成一个全音符的时长:
metro :: Int -> Dur -> DurT
metro setting dur = 60/(fromIntegral setting * dur)因此, 例如, metro 96 qn创建了一个每分钟96个四分之一音符的节奏.除了上下文, 我们还需要知道使用的奏者(player); 也就是说, 我们需要一个从一个Music值中的每个PlayerName (一个字符串) 到实际使用的奏者的映射. 奏者的细节将会在本章之后进行解释 (第9.2节). 暂时我们只是定义一个类型别名来捕获从PlayerName到Player的映射:
type PMap a = PlayerName -> Player aEuterpea定义了一个叫做perform的无奏者默认演出函数, 其将一个Music值转换为一个Performance. 我们将会拓展Euterpea的既存机能, 通过创建一个新的演出函数hsomPerform:
hsomPerform :: PMap a -> Context a -> Music a -> Performance于是, hsomPerform pm c m是由本章我们将会探索Music数据类型以及定义于其上的函数的诸多性质, 这些性质共同构成了某种音乐代数. 利用这种代数我们可以按照保持意义的方式对于计算机音乐程序进行推理, 变换和优化.
设我们有了两个值m1 :: Music Pitch和m2 :: Music Pitch, 并且我们想要知道他们是否是相等的. 如果我们将其仅仅视为Haskell的值, 那么我们可以轻易地编写一个函数, 其递归地进行比较以看出它们是否在每个层次上都是相同的, 一路向下直至休止和音符原语. 这实际上正是Haskell函数==所做的事情. 例如, 如果:
m1 = c 4 en :+: d 4 qn
m2 = retro (retro m1)那么m1 == m2为True.不幸的是, 正如我们在上一章所看到的, 如果我们反转一个平行复合, 事态就和之前不太一样了. 例如:
retro (retro (c 4 en :=: d 4 qn))(rest 0 :+: c 4 en :+: rest en) :=: d 4 qn{译注: 稍加观察即可确信这两者表示了相同的音乐.}
除此以外, 正如我们在第1章中所简要讨论的, 存在标准的Haskell等价不足以捕获的音乐性质. 例如, 我们会期望以下两个音乐值听起来相同, 不管m1, m2, m3的实际值是什么:
(m1 :+: m2) :+: m3
m1 :+: (m2 :+: m3)换言之, 我们期望运算符:+:是结合的.问题在于, 作为数据结构, 这两个值的确在一般情况下不是相等的; 实际上, 没有有限的值能够赋给m1, m2, m3以使得它们相等.
脱离这种两难的路在于定义新的相等概念, 其应该捕获演出(performance)相同的事实. 换言之, 如果两个东西听起来相同, 那么它们必然是音乐等价的.
因此, 我们定义了一种音乐等价的形式概念:
m1和m2是等价的, 记作文法描述了形式语言. 我们既可以设计该语言的识别器 (或者说parser), 也可以设计一个生成器, 其可以生成该语言的句子. 既然我们对于使用文法生成音乐感兴趣, 我们将只会关心生成文法.
一个生成文法是一个四元组, 其中:
Lindenmayer系统或者说L系统是生成文法的例子, 但是在两个方面有所不同:
Lindenmayer是一位生物学家和数学家, 而他使用L系统来描述特定生物有机体的生长 (例如植物, 特别是藻类).
我们将会限制我们的讨论于具有以下额外特征的L系统:
我们将既会考虑确定性文法, 又会考虑非确定性文法. 确定性文法对于字母表中的每个非终结符恰有一个产生式与之对应, 而非确定性文法可能有多于一个, 因而我们需要某种方式来在它们之间进行选择.
一个简单的上下文无关的确定性的文法框架可以在Haskell中设计如下. 我们将产生式的集合表示为符号/符号列表序对的列表:
data DetGrammar a = DetGrammar a -- start symbol
[(a, [a])] -- productions
deriving Show为了生成相继的句型, 我们需要定义一个函数, 给定一个文法, 返回一个符号列表的列表:
detGenerate :: Eq a => DetGrammar a -> [[a]]
detGenerate (DetGrammar st ps) = iterate (concatMap f) [st]
where f a = maybe [a] id (lookup a ps)注意到每一步我们都是并行
扩展每个符号的, 这使用了concatMap. 这种过程的重复则是由iterate所完成的. 同时我们也要注意到产生式的列表本质上是一个关联列表, 因而Data.List的库函数lookup相当适用于寻找所要找的产生式规则. 最后, 我们应该注意到又一次高阶函数的运用使得定义简明而高效.
作为使用这个简单程序的例子, 考虑以下受到藻类生长模式所启发的Lindenmayer文法:
redAlgae = DetGrammar 'a'
[('a', "b|c"), ('b', "b"), ('c', "b|d"),
('d', "e\\d"), ('e', "f"), ('f', "g"),
('g', "h(a)"), ('h', "h"), ('|', "|"),
('(', "("),
(')', ")"), ('/', "\\"),
('\\', "/")
]然后, detGrammar redAlgae给出了我们所想要的结果——或者, 为了使其看上去更好, 我们可以:
t n g = sequence_ (map putStrLn (take n (detGenerate g)))例如, t 10 redAlgae会产生:a
b|c
b|b|d
b|b|e\d
b|b|f/e\d
b|b|g\f/e\d
b|b|h(a)/g\f/e\d
b|b|h(b|c)\h(a)/g\f/e\d
b|b|h(b|b|d)/h(b|c)\h(a)/g\f/e\d
b|b|h(b|b|e\d)\h(b|b|d)/h(b|c)\h(a)/g\f/e\d上一节的设计只捕获了确定性上下文无关文法, 并且生成器只考虑了作为L系统特征的并行产生.
我们还希望考虑非确定性文法, 用户可以在其中描述选择特定规则的概率, 以及可能的非上下文无关 (即上下文相关) 文法. 因此, 我们将以更抽象的方式表示生成文法, 作为一个数据结构, 其具有一个(隐式的, 多态的)字母表中的起始句子和一个产生式规则的列表:
data Grammar a = Grammar a -- start sentence
(Rules a) -- production rules
deriving Show产生式规则是将字母表中的句子转换为其他句子的指令. 规则集要么是一组均匀分布的规则 (意味着具有相同左侧的规则被选中的概率相等), 要么是一组随机规则 (每个规则都与一个概率配对). 一个特定的规则由一个左侧和一个右侧构成.data Rules a = Uni [Rule a]
| Sto [(Rule a, Prob)]
deriving (Eq, Ord, Show)
data Rule a = Rule {lhs :: a, rhs :: a}
deriving (Eq, Ord, Show)
type Prob = Double{译注: DetGrammar和Grammar的类型参数的意蕴是不同的, 前者的类型参数代表符号的类型, 后者的类型参数代表句子的类型. 所以说, 我不太理解为什么作者要称后者的类型参数为字母表.}我们不得不解决的关键子问题之一是
本章我们研究了声音的本质以及其作为信号的数学表示. 我们也讨论了信号的离散数字表示, 其构成了现代声音合成与音频处理的基础.
在学习数字音频之前, 首先了解什么是声音是很重要的. 从本质上讲, 声音是空气的快速压缩与松弛, 以波的形式从声音的物理来源传播, 最终到达我们的耳朵. 声音的来源可以是声带的振动 (产生语音或歌唱), 扬声器振膜的振动, 汽车发动机的振动, 钢琴或小提琴中琴弦的振动, 萨克斯管中簧片的振动, 演奏小号时嘴唇的振动, 甚至是我们拍手时产生的(短暂而混乱的)振动. 空气 (或螺旋弹簧) 的这种压缩与松弛
被称为纵波, 在纵波中, 振动方向与波的传播方向平行. 与之相对, 一端固定另一端被摇动的绳子以及海洋中的波浪都是横波的例子, 在横波中, 绳子或水的运动方向与波传播的方向是垂直的.
如果声音的频率和振幅处在合适的范围内, 我们就能够听到它, 即它属于可听声音. 听觉
产生于振动的空气波使我们的鼓膜振动, 从而刺激进入大脑的神经. 超出我们听觉范围的声音 (即振动速度过快, 无法引发任何神经冲动) 称为超声波, 而低于我们听觉范围的声音则称为次声波.
仍然停留在模拟世界中, 声音还可以通过麦克风 (简称mic
) 转换为电信号. 常见的几种麦克风包括:
也许用图示来表示波形的最常见最自然的方式——无论是声波还是电波, 纵波还是横波——就是绘制振幅随时间变化的图像. 例如, 图18.1显示了一个每秒个周期的正弦波, 其振幅在和之间变化. 正弦波严格符合数学中正弦函数的定义, 同时, 正如我们很快将看到的那样, 它也与大多数乐器所产生的声音振动有着密切的关系. 在本书余下的内容中, 我们将把正弦波简单地称为sine wave. {译注: 当然, 对于译文来说, 我们就会使用正弦波这个术语.}