/* Remarks You might want to browse this file by "sted." このファイルを sted というエディタで閲覧するといい感じかも知れません。 http://www008.upp.so-net.ne.jp/momotan/sted.html supported MML commands: - (Go to http://platz.jp/~mml2mid/ for reference) - abcdefg - r - + - - ^ . - < > - k M E p - q l v o _ P X @ ~ - C t extended MML command: - T means Track. MML's track notations are not supported. don'ts: - a0 - a.8 specifications: - The player has 16 channels and the parser has 32 tracks by default. - The parser is designed to play a looped music. It also provides OneShot MML parser to play shorter excerpts. (It's useful for sound effects a bit!) - The parser can play only one note in a track at one time. - The parser sends NoteOff when it attempts to play more than one same note in a channel. (it's the parser's spec; the player module can play more than one same note if you absolutely need.) This means if you hope to have multiple concurrent sound effects like explosion noise, you must use different notes in that channel, more than one Track&channel, and/or directly manipulate player_note. - @n : send exact value n to the driver (GM design.) - the parser ignores whitespaces (Charcode <= $20). - Channel command (Cn) should come earlier than any other control changes or notes to correctly complete channel mapping. - Tracks are supposed to be approximately the same length, but not necessarily be. The parser halts parsing shorter Tracks until the longest Track ends. - OneShot MML parser ignores Track numbers though you can still have multiple Tracks and leave Track numbers in your MML anyway. The parser automatically assigns free Track to each MML string divided by 'T' without identifying those Tracks. Hence, you must make a Track consecutive, which is not separated by time frames. - OneShot MML parser searches for a free Track from the tail of the array. (e.g. if there are 32 Tracks, then it checks Track[31], Track[30], ..) - Your music's tempo is based on fTempo@player. (see player_ini) - OneShot's tempo is based on fTempoOneShot@parser. (see parser_ini) - fRealTime@parser: flag for RealTime playing mode If this flag is on, the parser tries to catch up with real-life time. So some notes might be skipped when the parsing process was interrupted and resumes. A good thing for using this mode is that note timings are always acculate and never mess up. If this flag is off, it is guranteed that the parser plays every note without skipping. However, you might experience slightly "drifting off" notes since the parser never truncate GateTime despite delays. (see parser_set_playmode) Q&A: - What does GM System On do? -> all note off -> reset control values - CC7 (vol) <- 100 - CC10(pan) <- 64 - CC11(exp) <- 127 - PitchB Rng <- +-2 - and more?? * Due to MIDI hardware design, you must wait for MIDI module for at least 500ms after each GM System On/GS Reset/XG Reset. /**/ #module "out" #define CHANNELS 16 #deffunc out_ini // flag if MIDI device is open fOpen = 0 // loading dlls and procs ll_libload fWinmm, "winmm.dll" ll_getproc fmidiOutOpen, "midiOutOpen", fWinmm ll_getproc fmidiOutReset, "midiOutReset", fWinmm ll_getproc fmidiOutClose, "midiOutClose", fWinmm ll_getproc fmidiOutShortMsg, "midiOutShortMsg", fWinmm ll_getproc fmidiOutSetVolume, "midiOutSetVolume", fWinmm ll_getproc fmidiOutGetVolume, "midiOutGetVolume", fWinmm // ll_getproc fmidiOutLongMsg, "midiOutLongMsg", fWinmm ll_getproc fmidiOutUnprepareHeader, "midiOutUnprepareHeader", fWinmm ll_getproc fmidiOutPrepareHeader, "midiOutPrepareHeader", fWinmm ll_libload fKernel32, "kernel32.dll" ll_getproc fSleep, "Sleep", fKernel32 // open device ll_getptr fHMidiOut ll_ret lPrm lPrm.1 = $ffffffff lPrm.2 = 0 lPrm.3 = 0 lPrm.4 = 0 ll_callfunc lPrm, 5, fmidiOutOpen // ll_ret lRet if lRet == 0 { fOpen = 1 ll_callfunc fHMidiOut, 1, fmidiOutReset } return #deffunc out_bye onexit // midiOutReset() & midiOutClose() if fOpen{ // ll_callfunc fHMidiOut, 1, fmidiOutReset // no sweat. ll_callfunc fHMidiOut, 1, fmidiOutClose fOpen = 0 } // free dll ll_libfree fWinmm ll_libfree fKernel32 return #deffunc out_put int, int, int if fOpen == 0 : return mref a1st, 0 mref a2nd, 1 mref a3rd, 2 lPrm.0 = fHMidiOut lPrm.1 = a1st lPrm.1 = a2nd<< 8 | lPrm.1 lPrm.1 = a3rd<<16 | lPrm.1 ll_callfunc lPrm, 2, fmidiOutShortMsg return #deffunc out_get_volume if fOpen == 0 : return mref lStat, 64 lPrm.0 = fHMidiOut ll_getptr lTmp ll_ret lPrm.1 ll_callfunc lPrm, 2, fmidiOutGetVolume lStat = lTmp return #deffunc out_set_volume int, int if fOpen == 0 : return mref aLeft, 0 mref aRight, 0 lPrm.0 = fHMidiOut lPrm.1 = aRight << 16 | aLeft ll_callfunc lPrm, 2, fmidiOutSetVolume return #deffunc out_put_long val, int // aSysEx, aLen if fOpen == 0 : return mref aSysEx, 24 mref aLen, 1 sdim lSysEx, aLen memcpy lSysEx, aSysEx, aLen // MIDIHDR dim lMidiHdrInt, 16 ll_getptr lSysEx ll_ret lMidiHdrInt.0 lMidiHdrInt.1 = aLen lMidiHdrInt.2 = aLen lMidiHdrInt.4 = 0 // call midiOutLongMsg lPrm.0 = fHMidiOut ll_getptr lMidiHdrInt ll_ret lPrm.1 lPrm.2 = 64 ll_callfunc lPrm, 3, fmidiOutPrepareHeader ll_callfunc lPrm, 3, fmidiOutLongMsg // from ちょくと's MIDI OUT module l1ms = 1 repeat 1000 if lMidiHdrInt.4 & 1 : break ll_callfunc l1ms, 1, fSleep loop ll_callfunc lPrm, 3, fmidiOutUnprepareHeader return /* typedef struct { LPSTR lpData; DWORD dwBufferLength; DWORD dwBytesRecorded; DWORD dwUser; DWORD dwFlags; struct midihdr_tag far * lpNext; DWORD reserved; DWORD dwOffset; DWORD dwReserved[4]; } MIDIHDR; /**/ #global #module "clock" #deffunc clock_ini ll_getproc ftimeGetTime, "timeGetTime", fWinmm@out ll_getproc ftimeBeginPeriod, "timeBeginPeriod", fWinmm@out ll_getproc ftimeEndPeriod, "timeEndPeriod", fWinmm@out lPrm = 1 ll_callfunc lPrm, 1, ftimeBeginPeriod return #deffunc clock_bye onexit lPrm = 1 ll_callfunc lPrm, 1, ftimeEndPeriod return #deffunc gettime2 ll_callfnv ftimeGetTime@clock ll_ret fElapsed return #global #module "player" #define NOTES 128 #deffunc player_note int, int, int, int // aChn aNote aVel aGate if fOpen@out == 0: return mref aChn, 0 mref aNote, 1 mref aVel, 2 mref aGate, 3 // Status Byte = $90 : NoteOn (but also used for NoteOff with vel=0) out_put $90 | aChn, aNote, aVel // (re)set expiration table if aVel > 0 { // NoteOn gettime2 fNoteExpiration.aChn.aNote = fElapsed@clock + aGate fNumOfNotes.aChn += 1 } else { // NoteOff fNoteExpiration.aChn.aNote = 0 fNumOfNotes.aChn -= 1 } return #deffunc player_control_change int, int, int // aChn aCtrlNum aCtrlVal if fOpen@out == 0 : return mref aChn, 0 mref aCtrlNum, 1 mref aCtrlVal, 2 // Status Byte = $b0 : Control Change out_put $b0 | aChn, aCtrlNum, aCtrlVal return #deffunc player_modulation int, int // aChn aProgVal mref aChn, 0 mref aProgVal, 1 lChn = aChn lProgVal = aProgVal player_control_change lChn, 1, lProgVal return #deffunc player_volume int, int // aChn aProgVal mref aChn, 0 mref aProgVal, 1 lChn = aChn lProgVal = aProgVal player_control_change lChn, 7, lProgVal return #deffunc player_panpot int, int // aChn aProgVal mref aChn, 0 mref aProgVal, 1 lChn = aChn lProgVal = aProgVal player_control_change lChn, 10, lProgVal return #deffunc player_expression int, int // aChn aProgVal mref aChn, 0 mref aProgVal, 1 lChn = aChn lProgVal = aProgVal player_control_change lChn, 11, lProgVal return #deffunc player_hold int, int // aChn aProgVal mref aChn, 0 mref aProgVal, 1 lChn = aChn lProgVal = aProgVal player_control_change lChn, 64, lProgVal return #deffunc player_reset_all_controller int // aChn mref aChn, 0 lChn = aChn player_control_change lChn, 121, 127 return #deffunc player_all_note_off int // aChn mref aChn, 0 lChn = aChn player_control_change lChn, 123, 127 repeat NOTES lNote = cnt if fNoteExpiration.lChn.lNote = 0 loop return #deffunc player_program_change int, int // aChn aProgNum if fOpen@out == 0 : return mref aChn, 0 mref aProgNum, 1 // Status Byte = $c0 : Program Change out_put $c0 | aChn, aProgNum, 0 return #deffunc player_pitchbend int, int // aChn aProgVal if fOpen@out == 0 : return mref aChn, 0 mref aProgVal, 1 lProgVal = aProgVal*16383/127 - 8192 // Status Byte = $e0 : PitchBend out_put $e0 | aChn, lProgVal & $7f, lProgVal & $7f00 >> 7 & $7f return // #deffunc player_sysex str, int // aSysEx, aLen mref aSysEx, 32 mref aLen, 1 lLen = aLen ll_bin fSysEx, aSysEx out_put_long fSysEx, lLen return #deffunc player_gm_system_on player_sysex "F0 7E 7F 09 01 F7", 6 return #deffunc player_gs_reset player_sysex "F0 41 10 42 12 40 00 7F 00 41 F7 ", 11 return #deffunc player_xg_reset player_sysex "F0 43 10 4C 00 00 7E 00 F7", 9 return #deffunc player_master_vol int // aMasterVol mref aProgVal, 0 int lMasterVol lMasterVol = aProgVal str lMasterVol, 17 player_sysex "F0 7F 7F 04 01 00 "+lMasterVol+" F7", 8 return // #deffunc player_check_if_expired // to be called in main playing loop gettime2 repeat CHANNELS@out lChn = cnt if fPlaying.lChn == 0 : continue repeat NOTES lNote = cnt if fNoteExpiration.lChn.lNote == 0 : continue if fNoteExpiration.lChn.lNote <= fElapsed@clock : player_note lChn, lNote, 0 loop loop return #deffunc player_enable_channel int, int // aChn, aVal(0:mute) mref aChn, 0 mref aVal, 1 fPlaying.aChn == aVal return // #deffunc player_reset dim fNoteExpiration, CHANNELS@out, NOTES // 128 notes //fNoteExpiration,Chn,Note == 0 : the note is off dim fNumOfNotes, CHANNELS@out player_gm_system_on return #deffunc player_ini sdim fSysEx, 128 fTempo = 120 player_reset dim fPlaying, CHANNELS@out repeat CHANNELS@out fPlaying.cnt = 1 loop return #deffunc player_bye onexit repeat CHANNELS@out lChn = cnt player_all_note_off lChn player_reset_all_controller lChn loop return #global #module "track_info" #define TRACKS 32 #deffunc track_info_reset int // aTrk mref aTrk, 0 fChannelMapper.aTrk = 0 poke o, aTrk, $05 poke l, aTrk, $04 poke k, aTrk, $64 poke q, aTrk, $5f // 95% _.aTrk = 0 return #deffunc track_info_ini dim fChannelMapper, TRACKS sdim o, TRACKS sdim l, TRACKS sdim k, TRACKS sdim q, TRACKS dim _, TRACKS repeat TRACKS track_info_reset cnt loop return #global #module "parser" #define MAX_TRACK_BUF 4000 #deffunc parser_ini player_reset // allocate and clear member fields sdim fTrkBuf, MAX_TRACK_BUF, TRACKS@track_info dim fTrkLen, TRACKS@track_info // also used to check if the Track is used dim fTrkOneShot, TRACKS@track_info // :boolean dim fTrkPtr, TRACKS@track_info // used in parser_proc dim fCmdExpiration, TRACKS@track_info fScale = 9,11,0,2,4,5,7 fTempoOneShot = 120 fRealTime = 0 return #deffunc parser_bye onexit return #deffunc parser_set_playmode int // aRealTime:boolean mref aRealTime, 0 fRealTime = aRealTime return #deffunc parser_set_music val, int // MML string, OneShot flag // lTrk: 1 based, lTmpTrk: 0 based mref aMML, 24 mref aOneShot, 1 if aOneShot == 0 { player_reset track_info_ini // clear buffer sdim fTrkBuf, MAX_TRACK_BUF, TRACKS@track_info dim fTrkLen, TRACKS@track_info dim fCmdExpiration, TRACKS@track_info } sdim lStr, 3 lTrackParserState = 0 lPtr = 0 repeat peek lCh, aMML, lPtr switch lCh case 0 break // end of string swbreak case 'T' lTrackParserState = 'T' swbreak case '0' case '1' case '2' case '3' case '4' case '5' case '6' case '7' case '8' case '9' switch lTrackParserState case 0 lTrk = 1 if aOneShot : gosub *parser_get_free_track lTrackParserState = 'M' case 'M' gosub *parser_track_concat swbreak case 'T' lTrk = lCh - '0' lTrackParserState = 'd' swbreak case 'd' lTrk = lTrk * 10 + lCh - '0' lTrackParserState = 'd' swbreak swend swbreak default switch lTrackParserState case 0 case 'T' lTrk = 1 if aOneShot : gosub *parser_get_free_track case 'd' case 'M' gosub *parser_track_concat swbreak swend lTrackParserState = 'M' swend lPtr++ loop // memorize each track's length repeat TRACKS@track_info lTrk = cnt strlen fTrkLen.lTrk, fTrkBuf.lTrk loop return *parser_track_concat // destroys lStr, lPtr, lTmpTrk if lCh <= $20 : return if (lCh>$7f)&(lCh<$a0)|(lCh>$df) : strmid lStr, aMML, lPtr, 2 : lPtr++ : else : strmid lStr, aMML, lPtr, 1 // lTrk == 0: no Track buffer available if lTrk>0{ lTmpTrk = lTrk - 1 fTrkBuf.lTmpTrk += lStr if aOneShot : fTrkOneShot.lTmpTrk = 1 } return *parser_get_free_track // destroys lTrk, lTmpTrk lTrk = 0 repeat TRACKS@track_info lTmpTrk = TRACKS@track_info - cnt - 1 if (fTrkOneShot.lTmpTrk != 0)|(fTrkLen.lTmpTrk != 0) : continue fTrkOneShot.lTmpTrk = 1 track_info_reset lTmpTrk fCmdExpiration.lTmpTrk = 0 lTrk = lTmpTrk + 1 break loop return #deffunc parser_proc fEoM = 0 gettime2 repeat TRACKS@track_info // Track number and Channel number lTrk = cnt lChn = fChannelMapper@track_info.lTrk // check if the Track is used/expired if fTrkLen.lTrk <= 0 : continue if fCmdExpiration.lTrk > fElapsed@clock : continue // check if every Track has reached the end fEoM = 1 repeat TRACKS@track_info fEoM = (fTrkPtr.cnt >= fTrkLen.cnt) | fTrkOneShot.cnt & fEoM loop if fEoM : break // go parse lDone = 0 repeat if lDone : break gosub *parser_peek gosub *parser_advance switch lCh case 'a' case 'b' case 'c' case 'd' case 'e' case 'f' case 'g' case 'r' if lCh != 'r' { // resolve velocity peek lVel, k@track_info, lTrk // resolve note peek lOct, o@track_info, lTrk lNote = lCh - 'a' lNote = lOct * 12 + fScale.lNote // sharp or flat? repeat gosub *parser_peek if (lCh != '+') && (lCh != '-') : break if lCh == '+' : lNote += 1 if lCh == '-' : lNote -= 1 gosub *parser_advance loop // tranpose lNote += _@track_info.lTrk } else { // temporary "rest note" value lNote = NOTES@player } // Gate time // BaseGate [msec] peek lBaseGate, l@track_info, lTrk if fTrkOneShot.lTrk : lTempo = fTempoOneShot : else : lTempo = fTempo@player lBaseGate = 240000 / (lTempo * lBaseGate) // go parse lGateParseState = 0 gosub *parser_get_gate // calc expiration time if fRealTime{ if fCmdExpiration.lTrk == 0 : fCmdExpiration.lTrk = fElapsed@clock fCmdExpiration.lTrk += lGate } else { gettime2 fCmdExpiration.lTrk = fElapsed@clock + lGate } // unless rest note and already expired.. if (lNote' peek lOct, o@track_info, lTrk poke o@track_info, lTrk, lOct + 1 swbreak case 'k' gosub *parser_read_digits poke k@track_info, lTrk, lDigits swbreak case 'M' gosub *parser_read_digits player_modulation lChn, lDigits swbreak case 'E' gosub *parser_read_digits player_expression lChn, lDigits swbreak case 'p' gosub *parser_read_digits player_panpot lChn, lDigits swbreak case 'q' gosub *parser_read_digits poke q@track_info, lTrk, lDigits swbreak case 'l' gosub *parser_read_digits poke l@track_info, lTrk, lDigits swbreak case 'v' gosub *parser_read_digits player_volume lChn, lDigits swbreak case 'o' gosub *parser_read_digits poke o@track_info, lTrk, lDigits swbreak case '_' lTranpose = 1 repeat gosub *parser_peek if (lCh != '+') && (lCh != '-') : break if lCh == '-' : lTranpose = lTranpose * -1 gosub *parser_advance loop gosub *parser_read_digits _@track_info.lTrk = lTranpose * lDigits swbreak case 'P' player_hold lChn, 127 swbreak case 'X' player_hold lChn, 0 swbreak case '@' // while digits seen gosub *parser_read_digits player_program_change lChn, lDigits swbreak case '~' gosub *parser_read_digits player_pitchbend lChn, lDigits swbreak case 'C' gosub *parser_read_digits fChannelMapper@track_info.lTrk = lDigits - 1 lChn = fChannelMapper@track_info.lTrk swbreak case 't' gosub *parser_read_digits fTempo@player = lDigits swbreak swend loop loop return *parser_get_gate // destroys lCh, lGate lRejected = 0 repeat switch lGateParseState case 0 lGate = lBaseGate lPrevGate = lBaseGate lLastAddedGate = lBaseGate swbreak case '^' lGate += lBaseGate lPrevGate = lBaseGate lLastAddedGate = lBaseGate gosub *parser_advance swbreak case '.' lGate += lPrevGate / 2 lPrevGate = lPrevGate / 2 gosub *parser_advance gosub *parser_peek if ('0' <= lCh) && (lCh <= '9'){ lRejected = 1 } swbreak case 'd' // while digit seen gosub *parser_read_digits lGate -= lLastAddedGate if fTrkOneShot.lTrk : lTempo = fTempoOneShot : else : lTempo = fTempo@player lGateToAdd = 240000 / (lTempo * lDigits) lGate += lGateToAdd lPrevGate = lGateToAdd lLastAddedGate = lGateToAdd swbreak swend if lRejected : break // transition gosub *parser_peek switch lCh case '^' lGateParseState = '^' swbreak case '.' lGateParseState = '.' swbreak default if ('0' <= lCh) && (lCh <= '9') { lGateParseState = 'd' } else { lRejected = 1 } swend if lRejected : break loop return *parser_read_digits // destroys lDigits lDigits = 0 gosub *parser_peek repeat if ('0' <= lCh) && (lCh <= '9'){ // read digits lDigits = lDigits * 10 + lCh - '0' gosub *parser_advance gosub *parser_peek } else { break } loop return *parser_peek // destroys lCh peek lCh, fTrkBuf.lTrk, fTrkPtr.lTrk return *parser_advance fTrkPtr.lTrk += 1 // advance pointer // stay at the end of the track buffer if already reached. if fTrkPtr.lTrk >= fTrkLen.lTrk { lDone = 1 // a parsing iteration done. // clear buffer if OneShot if fTrkOneShot.lTrk { fTrkOneShot.lTrk = 0 track_info_reset lTrk memset fTrkBuf.lTrk, 0, fTrkLen.lTrk fTrkLen.lTrk = 0 } fTrkPtr.lTrk = fTrkLen.lTrk } return #deffunc parser_rewind repeat TRACKS@track_info lTrk = cnt if fTrkOneShot.lTrk == 0 { fTrkPtr.lTrk = 0 fCmdExpiration.lTrk = 0 //player_all_note_off fChannelMapper@track_info.lTrk } loop return #global #module "music" #deffunc music_ini sdim imm, 128 imm = {" T C4 @4 l24 q60 rrg>c16 T C4 @4 l24 q60 rere16 T C4 @4 l24 q60 crrc16 "} sdim crono, 800 crono ={" T1 @48q14 g8r4g8r4.f+8 g8r4g8r4.f+8 e8r4e8r4.d8 e8r4e8r4.d8 T2 @48q14 e8r4e8r4.d8 e8r4e8r4.d8 c+8r4c+8r4.c+8r4c+8r4. T3 @48q14 >c8r4c8r4.c8r4c8r4. q90 c^^ | d^d q75 c de | q90 f^g q75 edc | q60 cd q90 c^^ | d^d q75 ec | d^^^ t100 fg | q90 a^>d q75 ccc.d q75 ccc. q90 c^^ | d^d q75 c de | q90 f^g q75 edc | q60 cd q90 c^^ | d^d q75 ec | d^^^ fg | q90 a^>d q75 ccc.d q75 ccc.