Javascript在宿主软件中演奏软件乐器
Primo Pan

JS与Logic Pro X这样的编曲宿主软件实现通信,听起来就是一个很酷的功能,这意味着你编曲时使用的MIDI信号完全可以由代码生成。尤其是有了p5和tf.js之类的前端库,在人机交互时可以将交互时产生的信号映射成MIDI信号生成音乐。

WebMidi库就是一个可以实现JS与MIDI设备通信的Javascript库。相当于在浏览器和MIDI设备之间架起了一座桥梁。我们通过Macbook系统自带的虚拟MIDI设备IAC Driver可以直接与编曲软件Logic Pro进行通信。

启动台-其他中找到音频MIDI设置

音频MIDI设置

Command+2打开MIDI工作室窗口,选定Bus 1端口,Apply!

然后打开Logic,新建软件乐器轨道,这个时候我们通过JS写的代码就可以通过虚拟MIDI导入到Logic中了

接下来编写代码,因为WebMidi基于http协议,我们需要把一个html文件挂到本地服务器上,然后引入WebMidi库,CDN或本地化后皆可,我这里将WebMidi本地化了

1
<script src="webmidi.iife.js"></script>

然后为WebMidi的初始化绑定一个事件,初始化函数如下

1
2
3
4
5
6
7
8
9
10
11
12
function init()
{
WebMidi.enable(err=>
{
if (err) console.log(err);
WebMidi.outputs.forEach(output=>{
if (output.name==="IAC Driver Bus 1")
window.output=output
})
},true)
startLoop()//开始循环
}

注意output.name要选定你的虚拟MIDI端口,然后在startLoop中我们就可以编写MIDI数据了

我们演奏一个音需要用到output.playNote()方法,该函数有三个参数,第一个是音高(Note),你可以选择0~127之间任意一个整数,也可以直接以字符串的形式比如"C#4"来代表这个音,或者传入一个Note对象。第二个参数是轨道参数,记得在Logic中配置每个乐器的轨道名。第三个参数是一个对象,对象中可以修改time,attack,duration,release等参数,和我们混音时这些参数所代表的含义基本一致。

下面是我第一次写的曲子,苏联时期新闻联播的主题曲《Forward Time》的一个小片段,制谱参考油管视频G. Sviridov - Time, Forward! | Piano Tutorial (100,000 special)。做了一个小demo,写到了副歌,感觉效果还行。有了这个基础,去制作更有意思的与音乐有关的前端交互作品便成了可能。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
function startLoop()
{
var step=1;
var step2=1;
setInterval(()=>{
if (step2/16<12) {
if (step === 1 || step === 9)
output.playNote("E2", 1, {duration: 200, attack: 0.8})
if (step === 3 || step === 11)
output.playNote("G2", 1, {duration: 200, attack: 0.8})
if (step === 5 || step === 13)
output.playNote("A#2", 1, {duration: 200, attack: 0.8})
if (step === 7 || step === 15)
output.playNote("C#3", 1, {duration: 200, attack: 0.8})
}
if ((step2/16>=4 && step2/16<5)||(step2/16>=6 && step2/16<7))
{
if (step===1)
{
output.playNote("F3",2,{duration:300,attack:0.9})
output.playNote("A#3",2,{duration:300,attack:0.9})
output.playNote("C#3",2,{duration:300,attack:0.9})
}
if (step===5)
{
output.playNote("F3",2,{duration:50,attack:0.7})
output.playNote("A#3",2,{duration:50,attack:0.7})
output.playNote("C#3",2,{duration:50,attack:0.7})
}
if (step===7)
{
output.playNote("F3",2,{duration:50,attack:0.7})
output.playNote("A#3",2,{duration:50,attack:0.7})
output.playNote("C#3",2,{duration:50,attack:0.7})
}
if (step===8)
{
output.playNote("F3",2,{duration:20,attack:0.7})
output.playNote("A#3",2,{duration:20,attack:0.7})
output.playNote("C#3",2,{duration:20,attack:0.7})
}
if (step===9)
{
output.playNote("G3",2,{duration:40,attack:0.9})
output.playNote("C4",2,{duration:40,attack:0.9})
output.playNote("E4",2,{duration:40,attack:0.9})
}
if (step===11)
{
output.playNote("F3",2,{duration:1100,attack:0.9})
output.playNote("A#3",2,{duration:1100,attack:0.9})
output.playNote("C#3",2,{duration:1100,attack:0.9})
}
}
if (step2/16>=8 && step2/16<9)
{
if (step===1)
{
output.playNote("F3",2,{duration:300,attack:0.9})
output.playNote("A#3",2,{duration:300,attack:0.9})
output.playNote("C#3",2,{duration:300,attack:0.9})
}
if (step===5)
{
output.playNote("F3",2,{duration:50,attack:0.7})
output.playNote("A#3",2,{duration:50,attack:0.7})
output.playNote("C#3",2,{duration:50,attack:0.7})
}
if (step===7)
{
output.playNote("F3",2,{duration:50,attack:0.7})
output.playNote("A#3",2,{duration:50,attack:0.7})
output.playNote("C#3",2,{duration:50,attack:0.7})
}
if (step===8)
{
output.playNote("F3",2,{duration:20,attack:0.7})
output.playNote("A#3",2,{duration:20,attack:0.7})
output.playNote("C#3",2,{duration:20,attack:0.7})
}
if (step===9)
{
output.playNote("G3",2,{duration:40,attack:0.9})
output.playNote("C4",2,{duration:40,attack:0.9})
output.playNote("E4",2,{duration:40,attack:0.9})
}
if (step===11)
{
output.playNote("F3",2,{duration:800,attack:0.9})
output.playNote("A#3",2,{duration:800,attack:0.9})
output.playNote("C#3",2,{duration:800,attack:0.9})
}
}
if (step2/16>=9 && step2/16<11)
{
if (step===1)
{
output.playNote("G3",2,{duration:40,attack:0.9})
output.playNote("C4",2,{duration:40,attack:0.9})
output.playNote("E4",2,{duration:40,attack:0.9})
}
if (step===3)
{
output.playNote("F3",2,{duration:800,attack:0.9})
output.playNote("A#3",2,{duration:800,attack:0.9})
output.playNote("C#3",2,{duration:800,attack:0.9})
}
if (step===9)
{
output.playNote("G3",2,{duration:40,attack:0.9})
output.playNote("C4",2,{duration:40,attack:0.9})
output.playNote("E4",2,{duration:40,attack:0.9})
}
if (step===11)
{
output.playNote("F3",2,{duration:800,attack:0.9})
output.playNote("A#3",2,{duration:800,attack:0.9})
output.playNote("C#3",2,{duration:800,attack:0.9})
}
}
if (step2/16>=11 && step2/16<12)
{
if (step===1)
{
output.playNote("G3",2,{duration:40,attack:0.9})
output.playNote("C4",2,{duration:40,attack:0.9})
output.playNote("E4",2,{duration:40,attack:0.9})
}
if (step===3)
{
output.playNote("F3",2,{duration:800,attack:0.9})
output.playNote("A#3",2,{duration:800,attack:0.9})
output.playNote("C#3",2,{duration:800,attack:0.9})
}
}
if (step2/16>=12 && step2/16<18)
{
if (step%2===1)
{
output.playNote("F#2",1,{duration:100,attack:0.9})
output.playNote("C#3",1,{duration:100,attack:0.9})
output.playNote("F#3",1,{duration:100,attack:0.9})
output.playNote("C#4",1,{duration:800,attack:0.9})
}
}
if (step2/16>=14 && step2/16<15)
{
if (step===1) {
output.playNote("C#5", 2, {duration: 1500, attack: 0.9})
output.playNote("F#5", 2, {duration: 1500, attack: 0.9})
output.playNote("A#5", 2, {duration: 1500, attack: 0.9})
output.playNote("C#6", 2, {duration: 1500, attack: 0.9})
}
}
if (step2/16>=15 && step2/16<16)
{
if (step===1) {
output.playNote("F#4", 2, {duration: 1500, attack: 0.9})
output.playNote("A#4", 2, {duration: 1500, attack: 0.9})
output.playNote("C#4", 2, {duration: 1500, attack: 0.9})
}
if (step===5) {
output.playNote("F#4", 2, {duration: 1500, attack: 0.9})
output.playNote("A#4", 2, {duration: 1500, attack: 0.9})
output.playNote("D#5", 2, {duration: 1500, attack: 0.9})
}
if (step===9) {
output.playNote("F#4", 2, {duration: 1500, attack: 0.9})
output.playNote("A#4", 2, {duration: 1500, attack: 0.9})
output.playNote("D#5", 2, {duration: 1500, attack: 0.9})
output.playNote("F#5", 2, {duration: 1500, attack: 0.9})
}
if (step===13)
{
output.playNote("A#4", 2, {duration: 800, attack: 0.9})
output.playNote("C#5", 2, {duration: 800, attack: 0.9})
output.playNote("A#5", 2, {duration: 800, attack: 0.9})
}
if (step===16)
{
output.playNote("C#5", 2, {duration: 1500, attack: 0.9})
output.playNote("C#6", 2, {duration: 1500, attack: 0.9})
}
}
if (step2/16>=16 && step2/16<17)
{
if (step===1)
{
output.playNote("D#5", 2, {duration: 1500, attack: 0.9})
output.playNote("F#5", 2, {duration: 1500, attack: 0.9})
output.playNote("A#5", 2, {duration: 1500, attack: 0.9})
output.playNote("D#6", 2, {duration: 1500, attack: 0.9})
}
}
if (step2/16>=16 && step2/16<17)
{
if (step===1) {
output.playNote("C#5", 2, {duration: 1500, attack: 0.9})
output.playNote("F#5", 2, {duration: 1500, attack: 0.9})
output.playNote("A#5", 2, {duration: 1500, attack: 0.9})
output.playNote("C#6", 2, {duration: 1500, attack: 0.9})
}
}
if (step2/16>=17 && step2/16<18)
{
if (step===1) {
output.playNote("D#4", 2, {duration: 1500, attack: 0.9})
output.playNote("A#4", 2, {duration: 1500, attack: 0.9})
output.playNote("D#4", 2, {duration: 1500, attack: 0.9})
}
if (step===5) {
output.playNote("F4", 2, {duration: 1500, attack: 0.9})
output.playNote("A#4", 2, {duration: 1500, attack: 0.9})
output.playNote("F5", 2, {duration: 1500, attack: 0.9})
}
if (step===9) {
output.playNote("F#4", 2, {duration: 1500, attack: 0.9})
output.playNote("A#4", 2, {duration: 1500, attack: 0.9})
output.playNote("F#5", 2, {duration: 1500, attack: 0.9})
}
if (step===13)
{
output.playNote("A#4", 2, {duration: 800, attack: 0.9})
output.playNote("C#5", 2, {duration: 800, attack: 0.9})
output.playNote("A#5", 2, {duration: 800, attack: 0.9})
}
if (step===16)
{
output.playNote("C#5", 2, {duration: 300, attack: 0.9})
output.playNote("C#6", 2, {duration: 300, attack: 0.9})
}
}
if (step2/16>=18 && step2/16<20)
{
if (step%2===1)
{
output.playNote("A#2",1,{duration:100,attack:0.9})
output.playNote("D#3",1,{duration:100,attack:0.9})
output.playNote("F#3",1,{duration:100,attack:0.9})
}
}

if (step2/16>=18 && step2/16<19)
{
if (step===1){
output.playNote("F#5", 2, {duration: 1500, attack: 0.9})
output.playNote("A#5", 2, {duration: 1500, attack: 0.9})
output.playNote("D#5", 2, {duration: 1500, attack: 0.9})
output.playNote("F#6", 2, {duration: 1500, attack: 0.9})
}}
if (step2/16>=19 && step2/16<20)
{
if (step===1){
output.playNote("D#5", 2, {duration: 1500, attack: 0.9})
output.playNote("F#5", 2, {duration: 1500, attack: 0.9})
output.playNote("A#5", 2, {duration: 1500, attack: 0.9})
output.playNote("D#6", 2, {duration: 1500, attack: 0.9})
}
if (step===9){
output.playNote("F5", 2, {duration: 1500, attack: 0.9})
output.playNote("A#5", 2, {duration: 1500, attack: 0.9})
output.playNote("F6", 2, {duration: 1500, attack: 0.9})
}
}
if (step2/16>=20 && step2/16<21)
{
if (step===1){
output.playNote("C#5", 2, {duration: 1500, attack: 0.9})
output.playNote("F5", 2, {duration: 1500, attack: 0.9})
output.playNote("G#5", 2, {duration: 1500, attack: 0.9})
output.playNote("C#6", 2, {duration: 1500, attack: 0.9})
}}
if (step2/16>=20 )
{
if (step%2===1)
{
output.playNote("C#2",1,{duration:100,attack:0.9})
output.playNote("C#3",1,{duration:100,attack:0.9})
}
}step++;
if (step>16)step=1;
step2++;

// step2++;
//if (step2>32) step2=1
},100)
}