# 使用编程记谱法合成音乐

[范例：用 aardio 演奏起风了](../../../example/Media/Audio/TheWind.html)

## 使用 sys.midiOut 库合成音乐

🅰 示例：

```aardio
import sys.midiOut;

//打开播放设备
var midiOut = sys.midiOut(); 

//播放简谱
midiOut.play("
    两只老虎,
    1__,2__,3__,1__, 
    两只老虎,
    1__,2__,3__,1__, 
    跑得快,
    3__,4__,5__,__,
    跑得快,
    3__,4__,5__,__,
    一只没有耳朵,
    5_,6_,5_,4_,3__,1__,
    一只没有尾巴,
    5_,6_,5_,4_,3__,1__, 
    真奇怪,
    2__,'5__,1__,__
");
```

## sys.midiOut 编程记谱规则

sys.midiOut 使用的编程记谱法基于简谱记号设计。


### 1. 数字音符：

中音使用简谱记号：`1,2,3,4,5,6,7`

高音在后面加一个单引号：`1',2',3',4',5',6',7'`

低音在音符前面加一个单引号：`'1,'2,'3,'4,'5,'6,'7`

### 2.  SPN 音名

编程记谱时可使用 sys.midiOut.notes 命名空间指定的所有音名，这些音名使用科学音高记号（Scientific pitch notation）。

音名与音符的对应关系如下：

| 音名 | 唱名 | 简谱音符 |
| --- | --- | --- |
C4 | do | 1
D4 | re | 2
E4 | mi | 3
F4 | fa | 4
G4 | sol| 5
A4 | la | 6
B4 | ti （si,xi） | 7

音名与唱名的对应关系可以变更，这里先不用管这些。

SPN 音名后面的数值越大表示越高的音，例如 C4（ 中央C ，简谱中的 1 ） 高八度就是 C5（ 高音 do，简谱 1 上面加一点），低八度的音就是 C3 （ 低音 do，简谱 1 下面加一点）。

注意：

- SPN 音名中的 `-1` 在 sys.midiOut 中需要省略
- SPN 音名中的升号 `♯`（Sharp） 用小写 `s` 替代

例如：`C-1♯` 在 sys.midiOut 里需要略写为 `Cs`

### 3. 文本记谱规则

- 分隔符：

    所有音符、指令、字幕可使用逗号、竖线、半角空格、制表符、换行之一分隔，忽略多余的空白字符。

- 消音：

    数字音符前面加负号表示消音，例如： `-5,-'5`。SPN 音名小写表示消音，例如：`g4,g3`。

- 延时：

    用下划线 `_` 表示一个延时单位（默认为 250 毫秒 ），前面的`数字音符`或`下划线`与后面的`下划线`可以连起来写，例如：`5___,5___` 。但是 `SPN 音名`必须单独写，不能与`下划线`或`数字音符`连起来写。

    和弦只要连续写多个音符就可以，例如 `midiOut.play("1,3,5, __, 2,4,6, __, 3,5,7, __, 4,6,1',______","C4"/* 1=C */,250/* ♩=120 */);`。

- 函数调用：

    记谱时可以直接调用 midiOut 的成员函数，函数名后必须有括号 () 且必须有参数，例如： `delay(1000),1__,2__,3__` 。

    以下是所有可用于编程记谱的函数：

    - `delay(millisecond)` 用于延时。
        * 参数 @millisecond  指定延时毫秒数。
    - `setVelocity(value)` 用于调整音量。
        * 参数 @value 可指定 0~127 范围的值，数字越大音量越大。
    - `changeInstrument(instrument）` 用于改变乐器，
        * 参数 @instrument 指定乐器序号，可用序号请看后面的附录。
        * 可选增加一个数值参数自定义影响的通道。
    - `afterTouch(pitch,pressure)` 指定单个音符按键的触后效果。
        * 参数 @pitch 参数可用一个字符串指定数字音符或 SPN 音名。
        * 参数 @pressure 参数可用 0~127 范围的值指定压力。
        * 可选增加一个数值参数自定义影响的通道。
    - `channelPressure(pressure)`  指定触后通道压力，影响所有音符按键。
        * 参数 @pressure 参数可用 0~127 范围的值指定压力。
        * 可选增加一个数值参数自定义影响的通道。
    - `cc(number,value) ` 发送 CC（Continuous controller） 消息
        * 参数 @number 指定编号。
        * 参数 @value 指定值。
        * 可选增加一个数值参数自定义影响的通道。
    - `pitchBend(value) ` 调整弯音轮。
        * 参数 @value 用一个小数指定音高弯曲比例。0 ~ 0.5 表示向下弯曲，0.5 ~ 1 表示向上弯曲
        * 可选增加一个数值参数自定义影响的通道。
    - `pitchBend2(value) `调整弯音轮。
        * 参数 @value 用 14 位整数指定 0~16383 范围的音高弯曲值。0 表示向下弯曲2个半音，16383 表示向上弯曲2个半音。
        * 可选增加一个数值参数自定义影响的通道。


    - ...... 更多可用函数请参考库函数文档。

- 其他数值：

    其他大于 127 的数值表示毫秒单位的延时
    
- 字幕   
    
    其他除`,;|`与半角空格、制表符、换行以外的字符表示字幕。  
    也可以用一对双引号包含字幕（双引号内所有字符识别为单个字幕，字幕总是会去掉首尾所有双引号）。
    
    可自定义 sys.midiOut 对象的 log 方法用于显示字幕。

🅰 示例：

```aardio
import sys.midiOut;
var midiOut = sys.midiOut(); 

//自定义字幕显示函数
midiOut.log = lambda (v) print(v)

//合成并播放音乐
midiOut.play("
1,150,
pitchBend(0.6), 弯音,
500, 延时500毫秒,
-1,停（音符前加负号表示消音）
1000
"); 
```

**自定义音高、拍子快慢：**

midiOut.play 函数可选用第 2 个`字符串参数`指定数字音符 `1` 对应的 SPN 音名，默认值为 `"C4"`。可选用第 3 个参数指定单个下划线对应的延时单位（默认为 250 毫秒 ）

我们将上面示例的数字音符 `1` 改为指向 `"E4"` （其他数字音符会自动调整音高），一个延时单位改为 125 毫秒（加快一倍），代码如下：

```aardio
import sys.midiOut;

var midiOut = sys.midiOut(); 

midiOut.play("
    两只老虎,
    1__,2__,3__,1__, 
    两只老虎,
    1__,2__,3__,1__, 
    跑得快,
    3__,4__,5__,__,
    跑得快,
    3__,4__,5__,__,
    一只没有耳朵,
    5_,6_,5_,4_,3__,1__,
    一只没有尾巴,
    5_,6_,5_,4_,3__,1__, 
    真奇怪,
    2__,'5__,1__,__
","E4",125); 
```

运行上面的示例，播放的音乐更轻快了。

### 4. 数组记谱

sys.midiOut 允许使用数组记谱，要点：

- 数组中必须使用 sys.midiOut.notes 命名空间的 SPN 音名代替数字音符，小写 spn 音名表示消音。
- 函数调用必须用一个数组表示，数组的第一个元素是函数名。
- 大于 127 的数值表示毫秒单位的延时。

示例：

```aardio
import sys.midiOut;
var midiOut = sys.midiOut(); 

//只能在 sys.midiOut.notes 命名空间内使用数组记谱
namespace sys.midiOut.notes{
	drm = [
		C4,//数组内不能用字符串表示音符
        150,
		["pitchBend",0.6], "弯音",
		500, "	延时 500 毫秒",
		c4,"停（小写 SPN 音名表示消音）",
		1000 
	]
}

midiOut.play(sys.midiOut.notes.drm); 
```

> 建议使用文本记谱法，不建议使用数组记谱法（写起来较麻烦）。

## 绘制桌面歌词

🅰 示例：

```aardio
//创建桌面歌词窗口
import win.util.lyric;
var lyric = win.util.lyric();
lyric.show();

//打开播放设备
import sys.midiOut;
var midiOut = sys.midiOut(); 

//定义显示歌词的函数
midiOut.log = function(str){
    lyric.text = str; 
}

//播放简谱
midiOut.play("
    两只老虎,
    1__,2__,3__,1__, 
    两只老虎,
    1__,2__,3__,1__, 
    跑得快,
    3__,4__,5__,__,
    跑得快,
    3__,4__,5__,__,
    一只没有耳朵,
    5_,6_,5_,4_,3__,1__,
    一只没有尾巴,
    5_,6_,5_,4_,3__,1__, 
    真奇怪,
    2__,'5__,1__,__
");  
```

上面的代码我们可自定义 sys.midiOut 对象的 log 方法用于显示歌词，并且使用了 win.util.lyric 扩展库显示桌面歌词。

## 多线程合奏音乐

合奏要点：

- sys.midiOut 对象允许作为参数传入不同的线程。  
同一进程中只能打开一个 sys.midiOut; 设备，因此多线程应当使用同一 sys.midiOut 对象。
- 建议在主线程（一般是界面线程）创建 sys.midiOut 对象。  
因为 `midiOut.play` 本身是阻塞运行而非异步运行，所以必须先启动其他工作线程，然后再在主线程调用 `midiOut.play` 才能实现多线程同时演奏。
- 当 sys.midiOut  对象传入不同线程时将绑定相同的音乐输出设备，但修改一个线程内 sys.midiOut  对象的属性不会影响到其他线程（ aardio 不同的线程有不同的运行环境 ）。在不同线程中应使用 sys.midiOut 对象的 channel 属性指定不同的通道，channel 可指定 `0~16` 范围的值。因为默认通道的值为 0, 因此除主线程以外其他线程的 channel 应指定为 `1~16` 范围的值，这样才能实现多通道合奏。 

🅰 示例：

```aardio
import sys.midiOut;
import win.util.lyric;

// 创建桌面歌词
var lyric = win.util.lyric();
lyric.show();

// 打开 MIDI 播放设备
var midiOut = sys.midiOut();

midiOut.log = function(v) {
    lyric.text = v;//显示字幕
};

// 创建工作线程播放鸟鸣效果
thread.invoke(
    function(midiOut) {
        
        // 指定不同的通道以实现合奏
        midiOut.channel = 3;
        
        // 选择乐器为鸟鸣音效（乐器编号 123）
        midiOut.changeInstrument(123);
        
        // 播放鸟鸣效果,音量适中,节奏较快
        midiOut.setVelocity(52);
        
        //鸟鸣 - 森林的生机
        midiOut.play("
            delay(1000), 5'_,6'_,5'_, 
            delay(500), 6'_,5'_,6'_, 
            delay(800), 5'_,6'_,5'_, 
            delay(1200), 6'_,5'_,6'_, 
            delay(600), 5'_,6'_,5'_, 
            delay(1000), 6'_,5'_,6'_, 
            delay(1500), 5'_,6'_,5'_, 
            delay(2000), 森林晨曦结束,
        ","C5",100); // 高音区,延时单位短促
       
    }, midiOut
);


// 伴奏
thread.invoke(
    function(midiOut) {
        midiOut.channel = 2;
        midiOut.changeInstrument(122);
        midiOut.setVelocity(50); 
        
        //伴奏 - 海浪声,
        midiOut.play("
            1____,5____, 
            1____,5____, 
            3____,6____, 
            1____,5____, 
            3____,6____, 
            1____,5____, 
            3____,5'____, 
            1____,1____,
        ","C3",500); 
    }, midiOut
);

// 在主线程中播放音乐，选择钢琴
midiOut.changeInstrument(0);

// 演奏乐曲，必须先运行其他线程，因为 midiOut.play 函数本身是阻塞运行的。
midiOut.play("
    清晨的第一缕阳光,
    1__,2__,3__,5__, 
    3__,2__,1__,5_, 
    森林中的鸟鸣,
    6__,5__,3__,2__, 
    1__,2__,3__,1__, 
    微风拂过树梢,
    5__,6__,5__,3__, 
    2__,1__,2__,5_, 
    晨雾渐渐散去,
    3__,5__,6__,5'__, 
    3__,2__,1__,1__,6000
","C4",300);

//关闭窗口
lyric.close();
```

## 切换乐器

我们还可以用代码调用 sys.midiOut 对象的 changeInstrument 函数选择不同的乐器，示例：

```aardio
import sys.midiOut;
var midiOut = sys.midiOut(); 

//选择八音盒，参数为乐器编号
midiOut.changeInstrument(10);
```

也可以在记谱时调用函数，示例：

```aardio
import sys.midiOut;
var midiOut = sys.midiOut();

midiOut.play( "
changeInstrument(10),
1___,
2___,
3___,
" );  
```

可用的乐器编号为 `0~127` 范围的数值，全部编号如下：

```txt
//钢琴
0 大钢琴（声学钢琴）
1 明亮的钢琴
2 电钢琴
3 酒吧钢琴
4 柔和的电钢琴
5 加合唱效果的电钢琴
6 羽管键琴（拨弦古钢琴）
7 科拉维科特琴（击弦古钢琴）

//色彩打击乐器
8 钢片琴
9 钟琴
10 八音盒
11 颤音琴
12 马林巴
13 木琴
14 管钟
15 大扬琴

//风琴
16 击杆风琴
17 打击式风琴
18 摇滚风琴
19 教堂风琴
20 簧管风琴
21 手风琴
22 口琴
23 探戈手风琴

//吉他
24 尼龙弦吉他
25 钢弦吉他
26 爵士电吉他
27 清音电吉他
28 闷音电吉他
29 加驱动效果的电吉他
30 加失真效果的电吉他
31 吉他和音

//贝司
32 大贝司（声学贝司）
33 电贝司（指弹）
34 电贝司（拨片）
35 无品贝司
36 掌击1
37 掌击2
38 电子合成1
39 电子合成2

//弦乐
40 小提琴
41 中提琴
42 大提琴
43 低音大提琴
44 弦乐群颤音音色
45 弦乐群拨弦音色
46 竖琴
47 定音鼓

//合奏/合唱
48 弦乐合奏音色1
49 弦乐合奏音色2
50 合成弦乐合奏音色1
51 合成弦乐合奏音色2
52 人声合唱“啊”
53 人声“嘟”
54 合成人声
55 管弦乐敲击齐奏

//铜管
56 小号
57 长号
58 大号
59 加弱音器小号
60 法国号（圆号）
61 铜管组（铜管乐器合奏音色）
62 合成铜管音色1
63 合成铜管音色2

//簧管
64 高音萨克斯风
65 次中音萨克斯风
66 中音萨克斯风
67 低音萨克斯风
68 双簧管
69 英国管
70 巴松（大管）
71 单簧管（黑管）

//笛
72 短笛
73 长笛
74 竖笛
75 排箫
76 Bottle Blow
77 日本尺八
78 口哨声
79 奥卡雷那

//合成主音
80 合成主音1（方波）
81 合成主音2（锯齿波）
82 合成主音3
83 合成主音4
84 合成主音5
85 合成主音6（人声）
86 合成主音7（平行五度）
87 +合成主音8（贝司加主音）

//合成音色
88 合成音色1（新世纪）
89 合成音色2（温暖）
90 合成音色3
91 合成音色4（合唱）
92 合成音色5
93 合成音色6（金属声）
94 合成音色7（光环）
95 合成音色8

//合成效果
96 雨声
97 音轨
98 水晶
99 大气
100 明亮
101 鬼怪
102 回声
103 科幻

//民间乐器
104 西塔尔（印度）
105 班卓琴（美洲）
106 三昧线（日本）
107 十三弦筝（日本）
108 卡林巴
109 风笛
110 民族提琴
111 山奈

//打击乐器
112 叮当铃
113 Agogo 钟
114 钢鼓
115 木鱼
116 太鼓
117 通通鼓
118 合成鼓
119 铜钹

//声音效果
120 吉他换把杂音
121 呼吸声
122 海浪声
123 鸟鸣
124 电话铃
125 直升机
126 鼓掌声
127 Q 声
```