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 提供了多个启动播放的方法:
midiOut.play(notes,baseNote,tempoMs,startDelay) 在当前线程异步启动播放(基于 win.setTimeout,依赖消息循环)midiOut.playSync(notes,baseNote,tempoMs) 直接同步播放midiOut.playAsync(notes,baseNote,tempoMs) 使用 thread.invoke 创建后台线程异步播放(计时不会被窗口消息阻塞打扰)参数用法:
1 的基准音。此参数既可以指定 "C4" 这类 SPN 音名,也可以直接指定 0~127 范围的 MIDI 音高数值。
默认值为C4。 C4 即音乐中最标准的中央 C(Middle C,MIDI 音高编号为 60)。250 毫秒。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
);
标准 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 使用的编程记谱法基于简谱记号设计。
中音使用简谱记号: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'。
编程记谱时可使用 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 下面加一点)。
注意:
-1 在 sys.midiOut 中需要省略♯(Sharp) 用小写 s 替代例如:C-1♯ 在 sys.midiOut 里需要略写为 Cs
负责解析文本乐谱的
sys.midiOut.parseNotes函数会自动将音名后的升号♯替换为s, 并将数字音符前的♯替换为#
分隔符:
所有音符、指令、字幕可使用逗号、竖线、半角空格、制表符、换行之一分隔,忽略多余的空白字符。
消音:
数字音符前面加负号表示消音,例如: -5,-'5。SPN 音名小写表示消音,例如:g4,g3。
延时:
用下划线 _ 表示一个延时单位(默认为 250 毫秒 ),前面的数字音符或下划线与后面的下划线可以连起来写,例如:5___,5___ 。但是 SPN 音名必须单独写,不能与下划线或数字音符连起来写。
音符的默认延时为 0,多个音符之间如果没有延时就会同时发音(和弦)。例如 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) 用于延时。
setVelocity(value) 用于调整音量。
changeInstrument(instrument) 用于改变乐器,
afterTouch(pitch,pressure) 指定单个音符按键的触后效果。
channelPressure(pressure) 指定触后通道压力,影响所有音符按键。
cc(number,value) 发送 CC(Continuous controller) 消息
pitchBend(value) 调整弯音轮。
pitchBend2(value)调整弯音轮。
其他数值:
其他大于 127 的数值表示毫秒单位的延时
字幕
其他除,;|与半角空格、制表符、换行以外的字符表示字幕。
也可以用一对双引号包含字幕(双引号内所有字符识别为单个字幕,字幕总是会去掉首尾所有双引号)。
可自定义 sys.midiOut 对象的 log 方法用于显示字幕。
🅰 示例:
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);
运行上面的示例,播放的音乐更轻快了。
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 扩展库显示桌面歌词。
合奏要点:
midiOut.play 本身是阻塞运行而非异步运行,所以必须先启动其他工作线程,然后再在主线程调用 midiOut.play 才能实现多线程同时演奏。0~16 范围的值。因为默认通道的值为 0, 因此除主线程以外其他线程的 channel 应指定为 1~16 范围的值,这样才能实现多通道合奏。 🅰 示例:
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 声