aardio 文档

使用编程记谱法合成音乐

范例:用 aardio 演奏起风了

使用 sys.midiOut 库合成音乐

sys.midiOut 对象

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__,__
","C4"/* 1=C */,250/* ♩=120 */);

midiOut 提供了多个启动播放的方法:

参数用法:

sys.midiOut.play 函数

sys.midiOut.play(notes,baseNote,tempoMs) 解析所有音符与指令并使用默认 MIDI 输出设备输出 MIDI 音乐。 在导入 win.ui 的线程内 sys.midiOut.play 调用 sys.midiOut 对象的 playAsync(notes,baseNote,tempoMs) 异步(无阻塞)播放音乐,未导入 win.ui 的线程则调用 playSync(notes,baseNote,tempoMs) 方法同步(阻塞)播放音乐。

此函数会自动打开默认 MIDI 输出设备, 如果主线程或当前线程之前已通过此方式打开默设备,则直接重用而非重复打开设备。

通过此函数成功打开 MIDI 设备的线程会被指定为设备的执有线程,执行线程退出时将负责自动释放设备。 执有线程是主线程时则允许其他线程的 sys.midiOut.play 共用此设备,否则将独占设备直到执有线程退出。 在执有线程内调用 sys.midiOut() 且不指定任何参数时会可以返回默认设备对象的副本(此副本的 close 方法将被置为空函数)。

sys.midiOut.play 是容错的,不必添加多余的 try 或 call 保护。 此函数在打开设备失败时(例如没有声卡,或设备被其他线程占用)忽略不执行任何操作,不会抛出异常。

示例:

import sys.midiOut;

sys.midiOut.play(
    "changeInstrument(0),
    '3_,#'2_,'3_,#'2_,'3_,''7_,'2_,'1_,''6__,_", // Für Elise
    "C6", // 1/Do = C6
    180 // tempoMs = 180
);

音符时值与自动 Note Off

标准 MIDI 把按键与松键表示为配对的 Note On、Note Off 事件。乐谱虽然不会把每次松键单独写出来,但每个音符自身包含“时值”;播放器会在时值结束时发送匹配的 Note Off。Note Off 表示松开琴键并触发音源的 Release(释放)阶段,不等于强行清空音频;释放后的余音长度由具体音色决定。midiOut.reset() 才是停止全部音符的紧急复位操作。

midiOut.play、midiOut.playSync、midiOut.playAsync 默认采用相同原则:音符或和弦后面的首个延时既推进播放时间,也作为这组音符的时值;延时结束时释放该时值内新按下、且尚未显式释放的音符。。例如 midiOut.play("changeInstrument(13),1_,5_","C4",80/*tempoMs*/) 表示两个音符各自演奏 80 毫秒,并在各自时值结束时释放,而不是一直保持到整段播放结束。和弦则在同一个延时结束时一起释放,例如 midiOut.play("1,3,5,__",60,100) 的 和弦时值为 200 毫秒。

直接调用 midiOut.noteOn(pitch,velocity,channel) 则保留 MIDI 原始语义,必须由调用方配对调用 midiOut.noteOff(pitch,channel) 或 midiOut.reset()。

如果需要跨越多个延时持续发音、编写独立复调声部,或完全按原始 MIDI 事件控制松键时机,可关闭自动时值处理,并在乐谱中使用负数音符明确消音:

midiOut.autoNoteOff = false; //禁止自动调用 midiOut.noteOff
midiOut.play("1,____,-1",60,100); //按下 1,保持 400ms,再松开 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

可在数字音符前添加 # 表示升记号(Sharp),如果有单引号必须写在 # 后面,例如 #'1,#'2,#3'。

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 下面加一点)。

注意:

例如:C-1♯ 在 sys.midiOut 里需要略写为 Cs

负责解析文本乐谱的 sys.midiOut.parseNotes 函数会自动将音名后的升号 ♯ 替换为 s, 并将数字音符前的 ♯ 替换为 #

3. 文本记谱规则

🅰 示例:

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 音名或 MIDI 音高数值,默认值为 "C4"。可选用第 3 个参数指定单个下划线对应的延时单位(默认为 250 毫秒 )

我们将上面示例的数字音符 1 改为指向 "E4" (其他数字音符会自动调整音高),一个延时单位改为 125 毫秒(加快一倍),代码如下:

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 允许使用数组记谱,要点:

示例:

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); 

建议使用文本记谱法,不建议使用数组记谱法(写起来较麻烦)。

绘制桌面歌词

🅰 示例:

//创建桌面歌词窗口
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 扩展库显示桌面歌词。

多线程合奏音乐

合奏要点:

🅰 示例:

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 函数选择不同的乐器,示例:

import sys.midiOut;
var midiOut = sys.midiOut(); 

//选择八音盒,参数为乐器编号
midiOut.changeInstrument(10);

也可以在记谱时调用函数,示例:

import sys.midiOut;
var midiOut = sys.midiOut();

midiOut.play( "
changeInstrument(10),
1___,
2___,
3___,
" );  

可用的乐器编号为 0~127 范围的数值,全部编号如下:

//钢琴
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 声

Markdown 格式