//midiOut 音乐输出 namespace sys; class midiOut{ ctor(deviceId,callback,flags){ if(deviceId === null) deviceId = -1/*_MIDI_MAPPER*/; if(type(deviceId)=="number"){ this.callback = callback; if(flags === null ){ var tc = type(callback); if( tc === "function" ){ this.callback = ..raw.tostdcall(callback,"void(ptr hmo,INT msg,INT instance,INT param1,INT param2)",this) flags = 0x30000/*_CALLBACK_FUNCTION */; } elseif( tc === "number" ){ flags = 0x10000/*_CALLBACK_WINDOW*/; } elseif( tc === "table" ){ flags = 0x50000/*_CALLBACK_EVENT*/; } } var handle = {ptr value} var err = ::Winmm.midiOutOpen(handle, deviceId,this.callback, 0,flags); this.handle = handle.value; if(!this.handle) { if(err){ var buf = ..raw.buffer(1024); if(0==::Winmm.midiOutGetErrorTextW(err,buf,512)) return null,..string.fromUtf16(buf); } return null; } } elseif(deviceId@==_meta) { this.handle = deviceId.handle; } else { this.handle = deviceId; } this.velocity = 127; }; note = function(pitch,velocity,channel){ if(pitch<0) return this.noteOff(-1-pitch,channel); else this.noteOn(pitch,velocity,channel); }; noteOn = function(pitch,velocity,channel){ channel = (channel:this.channel:0); if(this.onNote) this.onNote(pitch,velocity,channel); return ::Winmm.midiOutShortMsg(this.handle ,(velocity << 16 ) | (pitch << 8 ) | 0x90 | channel ); }; noteOff = function(pitch,channel){ return ::Winmm.midiOutShortMsg(this.handle ,(pitch << 8 ) | 0x80 | (channel:this.channel:0)) ); }; changeInstrument = function(i,channel){ return ::Winmm.midiOutShortMsg(this.handle ,(i << 8 ) | 0xC0 | (channel:this.channel:0)) ); }; afterTouch = function(pitch,pressure,channel){ if(type(pitch)=="string") pitch = parseNotes(pitch)[1]; return ::Winmm.midiOutShortMsg(this.handle ,(pressure << 16 ) | (pitch << 8 ) | 0xA0 | (channel:this.channel:0)) ); }; channelPressure = function(pressure,channel){ return ::Winmm.midiOutShortMsg(this.handle ,(pressure << 8 ) | 0xD0 | (channel:this.channel:0)) ); }; cc = function(number,value,channel){ return ::Winmm.midiOutShortMsg(this.handle ,(value << 16 ) | (number << 8 ) | 0xB0 | (channel:this.channel:0)) ); }; pitchBend = function(value,channel){ value = value * 16383; return ::Winmm.midiOutShortMsg(this.handle ,(( (value>>7) & 2#1111111 ) << 16)| ((value & 2#1111111 ) << 8 ) | 0xE0 | (channel:this.channel:0)) ); }; pitchBend2 = function(value,channel){ return ::Winmm.midiOutShortMsg(this.handle ,(( (value>>7) & 2#1111111 ) << 16)| ((value & 2#1111111 ) << 8 ) | 0xE0 | (channel:this.channel:0)) ); }; shortMsg = function(status,data1,data2){ if(data1!==null){ status = ((data2:0) << 16 ) | (data1 << 8 ) | status } return ::Winmm.midiOutShortMsg(this.handle,status); }; msg = function(msg,dw1,dw2){ ::Winmm.midiOutMessage(this.handle,msg,dw1,dw2); }; delay = function(ms){ ..thread.delay(ms); }; log = function(){}; xcall = function(v){ var t = type(v); if(t==="table"){ this[v[1]](..table.unpack(v,2)); } elseif( t == "string"){ this.log(v); } elseif( t == "number"){ if(v>127) ..thread.delay(v); else this.note(v,this.velocity); } else { return false; } }; play = function(notes,duo,delay){ if(type.isString(notes)){ notes = parseNotes(notes,duo,delay); } for(k,pitch in notes){ this.xcall(pitch); } }; setVelocity = function(v){ this.velocity = v; }; setVolume = function(v){ ::Winmm.midiOutSetVolume(this.handle,v); }; getVolume = function(msg){ var v = {INT value} if(0==::Winmm.midiOutSetVolume(this.handle,v)){ return v.value; } }; reset = function(){ ::Winmm.midiOutReset(this.handle); }; close = function(){ if(this.handle){ ::Winmm.midiOutClose(this.handle); this.handle = null; } }; @_meta; } namespace midiOut{ _meta = { _serialize = function(kernelCall){ if(kernelCall) return ..string.format("global.import('sys.midiOut') : sys.midiOut(topointer(0x%p))",owner.handle );; } } ::Winmm := ..raw.loadDll("Winmm.dll"); numDevice = lambda() ::Winmm.midiOutGetNumDevs(); eachDevice = function(){ var max = ::Winmm.midiOutGetNumDevs(); var idx = -1; return function(){ idx++; if(idx|<'>","'"); exp = ..string.replace(exp,",",","); exp = ..string.crlf(exp,'\n'); var items = ..string.split(exp,',|\n'); if(delay===null) delay=250; if(type(duo)=="string") duo = ..sys.midiOut.notes[duo]; if(!duo) duo = ..sys.midiOut.notes.C4; var numNotes = { ["1"] = duo; ["#1"] = duo+1; ["2"] = duo+2; ["#2"] = duo+3; ["3"] = duo+4; ["#3"] = duo+5; ["4"] = duo+5; ["#4"] = duo+6; ["5"] = duo+7; ["#5"] = duo+8; ["6"] = duo+9; ["#6"] = duo+10; ["7"] = duo+11; ["#7"] = duo+12; } var repeat = ..string.repeat; var octaves = {}; //允许在数字音符前增加最多 5 个单引号 for(i=1;5;1){ for(k,v in numNotes ){ if(#k==1){ octaves[repeat(i,"'")++k]=v-(12*i) } elseif(#k==2){ octaves["#"+repeat(i,"'")++k[[2]] ]=v-(12*i) } } } //允许在数字音符后增加最多 5 个单引号 for(i=1;5;1){ for(k,v in numNotes ){ if(#k==1){ octaves[k++repeat(i,"'")]=v+(12*i) } elseif(#k==2){ octaves["#"+k[[2]] ++ repeat(i,"'")]=v+(12*i) } } } ..table.assign(numNotes,notes,octaves); numNotes["0"] = {"reset"}; var ret = []; var methodParams; for(k,v in items){ if(!#v) continue; if(methodParams){ methodParams = methodParams + "," + v; var method,args = ..string.match(methodParams,"^(\w+)(%())$"); if(method && args){ try{ ..table.push(ret,{method,eval(..string.trim(args,"()"))}); } catch(e){ ..table.push(ret,e); } methodParams = null; } } elseif(numNotes[v] && v!="_") ..table.push(ret,numNotes[v]); elseif( v[1]='-'#) { v = ..string.right(v,-2); if(numNotes[v]) ..table.push(ret,-(numNotes[v]+1)); else { var pitch,delays = ..string.match(v,"^(#?[\d']+)(_+)$"); if(pitch && delays){ if(!numNotes[pitch]) { ..table.push(ret,v); } else{ var d = delay * #delays; ..table.push(ret,pitch!="0"?-(numNotes[pitch]+1):numNotes[pitch],d<=127 ? {"delay",d} : d); } } else { var pitch = ..string.match(v,"^(#?[\d']+)‗$"); if(pitch && numNotes[pitch]){ ..table.push(ret,pitch!="0"?-(numNotes[pitch]+1):numNotes[pitch],{"delay",delay/2}); } else { ..table.push(ret,"_"+v); } } } } else{ if(..string.find(v,"^_+$")){ var d = delay * #v; ..table.push(ret,d<=127 ? {"delay",d} : d); } else { var pitch,delays = ..string.match(v,"^(#?[\d']+)(_+)$"); if(pitch && delays){ if(!numNotes[pitch]) ..table.push(ret,v); else{ var d = delay * #delays; ..table.push(ret,numNotes[pitch],d<=127 ? {"delay",d} : d); } } else{ var pitch = ..string.match(v,"^(#?[\d']+)‗$"); if(pitch && numNotes[pitch]){ ..table.push(ret,numNotes[pitch],{"delay",delay/2}); } else{ var d = tonumber(v); if(d)..table.push(ret,d); else{ var method,args = ..string.match(v,"^(\w+)(%())$"); if(method && args){ try{ ..table.push(ret,{method,eval(..string.trim(args,"()"))}); } catch(e){ ..table.push(ret,e); } } elseif(v=="‗"){ ..table.push(ret, {"delay",delay/2} ); } elseif(#v) { methodParams = ..string.match(v,"^\w+\([^\)]+"); if(!methodParams)..table.push(ret,v); } } } } } } } return ret; }; parseNotesToFrequency = function(exp,duo,delay){ var notes = parseNotes(exp,duo,delay); var r = [] var d,f = 0; for(k,v in notes){ if( type(v) == "number"){ if(v>127) d+=v; //计算当前音符的延时 else{ if(d ) ..table.push(r,{freq=f,duration=d}); //保存当前音符 f = (v>0) ? ..math.round(440 * 2**((v - 69) / 12)): 0 ;//记录下一个音符 d = 0; //计算下一个音符的延时 } } } if(d ) ..table.push(r,{freq=f,duration=d}); return r; } } //@guide [使用编程记谱法合成音乐](doc://library-guide/std/sys/midiOut.md) /**intellisense() sys.midiOut = MIDI 音乐输出设备 sys.midiOut( = 打开 MIDI 音乐输出设备。\n成功返回设备对象,失败返回 null,错误信息。\n返回的设备对象可传到其他线程,或复制多个副本对象,\n副本对象可指定不同的 channel 属性,\n并可调用 changeInstrument 函数切换不同的乐器,\n可实现多线程多乐器多通道合奏\n\n在同一进程中如果未关闭设备则不能重复打开,\n不同进程可以打开同一设备 sys.midiOut(.(deviceId,callback,flags) = 打开 MIDI 输出设备。\n所有参数都可以省略,一般不需要了解,用法细节请查看源码。\n\n可选用 @deviceId 指定设备 ID\n设备 ID 从 0 开始且小于 sys.midiOut.numDevice 函数返回值,\n省略则为默认值 -1,也就是 _MIDI_MAPPER sys.midiOut(.(sysMidiOutObject) = 参数指定其他 sys.midiOut 对象,\n返回复制的副本对象。\n副本对象可指定不同的 channel 属性,\n并可调用 changeInstrument 函数切换不同的乐器 sys.midiOut.eachDevice() = @for deviceId,capabilities in sys.midiOut.eachDevice(){ __/*循环遍历所有 MIDI 输出设备,\ndeviceId 为设备 ID,capabilities 为 MIDIOUTCAPS 结构体*/ } sys.midiOut() = !sysMidiOut. sys.midiOut.notes = 定义了全部音符的命名空间,\n键为 SPN 音名,值为音高(pitch)。\n音名中的 -1 省略,升号 ♯(Sharp) 用小写 s 替代。\n例如:C-1♯ 略写为 Cs\n\n可用 namespace sys.midiOut.notes { } 语句打开此命名空间使用音名\n\n此命名空间还包含所有音名的小写音名,\n小写音名为对应音高取负并减 1。\n正值作为音符参数传给 note 函数演奏该音符,\n负值作为音符参数传给 note 函数停止演奏对应音符\n\n此命名空间的下划线 _ 的值为 250,\n传入 note,xcal 等函数可延时 250 毫秒 sys.midiOut.parseNotes( = 解析字符串格式乐谱并返回 play 函数支持的音符数组,\n所有音符或指令使用逗号、竖线、换行之一分开,忽略空格与制表符。\n不符合格式要求的项一律作为字幕传给 log 函数输出 sys.midiOut.parseNotes(.(notes,do,delay) = 解析字符串格式乐谱并返回 play 函数支持的音符数组。\n参数 @notes 指定一个字符串,\n在字符串中所有音符或指令使用逗号、竖线、换行之一分开,忽略空格与制表符。\n除了支持 sys.midiOut.notes 定义的全部音名,\n也可以使用简谱记号 1,2,3,4,5,6,7 表示音符,\n数字音符后加 N(小于 5 )个单引号表示提高 N 个八度音,\n数字音符前加 N(小于 5 )个单引号表示降低 N 个八度音。\n数字音符前再加 # 号表示升高半个音,0 为休止符。\n下划线则表示一个延时单位,其他数值也表示延时(毫秒),\n前面的音符(或下划线)与后面的下划线可以连起来写。\n双下划线 ‗ 表示 _ 的延时取半,多个 ‗ 不能连着写。\n支持函数语法,例如 "1,delay(120)" \n\n如果 @notes 为字符串,可选用 @duo 参数指定 1 音的音高,默认为"C4",\n可选用 @delay 参数指定下划线表示的延时单位,默认 250 毫秒 sys.midiOut.parseNotesToFrequency(.(notes,do,delay) = 首先将调用参数传给 sys.midiOut.parseNotes 函数并取得返回值。\n然后将返回值转换为包含频率(freq)与时长(duration)字段的对象数组。\n可将所有频率与时长顺序传给 ::Kernel32.Beep 函数作为调用参数以播放声音。 end intellisense**/ /**intellisense(!sysMidiOut) channel = 默认通道,可指定 0~16 范围的值。\n如果将 sys.midiOut 传入不同的线程,\n或以 sys.midiOut 为构造参数复制对象,\n则可指定不同的 channel 属性,切换不同的乐器,\n实现多通道合奏 shortMsg( = 发送短消息 shortMsg(.(status,data1,data2) = 发送短消息。\n@status 指定数值,0xC0 为切换乐器,0x90 为发送音符。\n@data1,@data2 可选指定数值 msg(.(msg,dw1,dw2) = 发送消息,\n@msg 指定消息 ID,其他参数为数值 setVolume(__) = 指定音量,0xFFFF 为最大值,0 为静音 getVolume() = 获取音量,0xFFFF 为最大值,0 为静音 reset() = 关闭所有音符 close() = 关闭所有线程的同一设备对象,\n不应关闭在其他线程已关闭的对象。\n对象回收时不会自动调用此函数。\n\n在同一进程中如果未关闭设备则不能重复打开,\n不同进程可以打开同一设备 noteOn( = 演奏音符 noteOn(.(pitch,velocity,channel) = 演奏音符。\n@pitch 可用 0~127 范围的值指定音高,相邻为半音,隔一为全音。\n@velocity 指定按键速度(音量强度),可指定 0~127 范围的值。\n@channel 可用 0~16 范围的值指定通道,省略则默认为当前通道 noteOff( = 停止演奏音符 noteOff(.(pitch,velocity,channel) = 停止演奏音符\n@pitch 可用 0~127 范围的值指定音高。\n@channel 可用 0~16 范围的值指定通道,省略则默认为当前通道 note( = 演奏或停止演奏 note(.(pitch,velocity,channel) = 演奏或停止演奏\n@pitch 如果指定 0~127 范围的值指定音高,则演奏指定音符。\n如果指定 -1~-128之间的值,则取正数并减一,然后停止演奏音高的音符。\n@velocity 指定按键速度(音量强度),可指定 0~127 范围的值。\n@channel 可用 0~16 范围的值指定通道,省略则默认为 0 changeInstrument(.(instrument,channel) = 切换乐器,\n@instrument 指定乐器编号,可选编号为 0~127 范围的值。\n乐器编号就搜索 MIDI 资料。\n@channel 可用 0~16 范围的值指定通道,省略则默认为当前通道。\n注意指定 @channel 并不会修改当前通道 afterTouch(.(pitch,pressure,channel) = 单个按键的触后效果,\n指击键后通过改变对键盘的压力来改变音色符。\n@pitch 用字符串指定数字音符或 SPN 音名,也可用 0~127 范围的值直接指定音高。\n@pressure 指定压力,可指定 0~127 范围的值。\n@channel 可用 0~16 范围的值指定通道,省略则默认为当前通道。\n注意指定 @channel 并不会修改当前通道 channelPressure(.((pressure,channel) = 所有按键的触后通道压力。\n@pressure 指定压力,可指定 0~127 范围的值。\n可选用 @channel 指定通道,省略则默认为当前通道 cc(.(number,value,channel) = 发送 CC(Continuous controller) 消息\n@number 指定编号,@value 指定值,\n可选用 @channel 指定通道,省略则默认为当前通道。\n注意指定 @channel 并不会修改当前通道 pitchBend(.(value,channel) = 调整弯音轮\n@value 用一个小数指定音高弯曲比例。\n0 ~ 0.5 表示向下弯曲,0.5 ~ 1 表示向上弯曲 pitchBend2(.(value,channel) = 调整弯音轮\n@value 用 14 位整数指定 0~16383 范围的音高弯曲值。\n0表示向下弯曲2个半音,16383表示向上弯曲2个半音 delay(__) = 延迟参数 @1 指定的毫秒数 setVelocity(__) = 使用参数 @1 设置 xcall,play 函数默认按键速度(音量),\n参数可指定 0~127 范围的值 velocity = xcall,play 函数默认按键速度(音量),\n参数可指定 0~127 范围的值 log = 用于输出字幕,默认是一个空函数。\n可指定为其他可输出内容的函数。\n此函数仅接受一个要输出的字符串参数 xcall(__) = 如果参数指定一个数组,\n则数组的第 1 个元素指定要调用的成员函数名,\n其他元素为调用参数\n\n例如: {"pitchBend",0.3}\n\n如果参数指定一个大于 127 的数值,则延时指定的时间,\n如果为 0~127 间的数值则按下指定音符。\n如果为 -1 到 -128 的数值则消音指定的音符。\n按下音符与消音可以使用 sys.midiOut.notes 命名空间的音名表示。\n如果为字符串则调用 log 函数输出。\n\n指定上述参数函数返回 true,指定其他数值返回 null,\n参数为其他数据类型会报错 play( = 解析并播放所有音符与指令,\n如果参数 @1 传入字符串格式的乐谱,\n则所有音符或指令使用逗号、竖线、换行之一分开,忽略空格与制表符。\n不符合格式要求的项一律作为字幕传给 log 函数输出 play(.(notes,do,delay) = 解析并播放所有音符。\n如果参数 @note 是一个数组,则循环调用 xcall 函数解析并播放音符。\n如果参数 @notes 是一个字符串,\n则首先调用 sys.midiOut.parseNotes 解析为音符数组,\n在字符串中所有音符或指令使用逗号、竖线、换行之一分开,忽略空格与制表符。\n除了支持 sys.midiOut.notes 定义的全部音名,\n也可以使用简谱记号 1,2,3,4,5,6,7 表示音符。\n数字音符后加 N(小于 5 )个单引号表示提高 N 个八度音,\n数字音符前加 N(小于 5 )个单引号表示降低 N 个八度音。\n数字音符前再加 # 号表示升高半个音,0 为休止符。\n下划线则表示一个延时单位,其他数值也表示延时(毫秒),\n前面的音符(或下划线)与后面的下划线可以连起来写。\n双下划线 ‗ 表示 _ 的延时取半,多个 ‗ 不能连着写。\n支持函数语法,例如 "1,delay(120)" \n\n如果 @notes 为字符串,可选用 @duo 参数指定 1 音的音高,默认为"C4",\n可选用 @delay 参数指定下划线表示的延时单位,默认 250 毫秒 onNote = @.onNote = function(pitch,velocity,channel){ __/*演奏指定音符时回调此函数,\n回调参数为 noteOn 函数的调用参数。\n这里最好不要阻塞线程,可用 thread.command 异步调用界面线程函数*/ } end intellisense**/