/* midi_examp.q: some sample midi functions */ include midi; import set; /****************************************************************************/ /* MIDI I/O devices. We use msAlsaSeq on Linux and MidiShare client #0 on Windows and OS X. Output goes to the port specified in the PORT variable. NOTE: When working under Windows, make sure that you have configured your msDrivers settings correctly. See the README file for details. */ def DRV = if pos "linux" sysinfo >= 0 then midi_client_ref "MidiShare/ALSA Bridge" else 0, IN = DRV, OUT = DRV, PORT = 0; /* Register a client which is used to send control messages. */ def CTRL = midi_open "Ctrl"; /* Register this application with MidiShare, connect it to IN, OUT and CTRL, and add a filter to get rid of active_sense messages. */ def APP = midi_open "Example", CONNECT = midi_connect IN APP || midi_connect APP OUT || midi_connect CTRL APP || midi_accept_type APP active_sense false; /****************************************************************************/ /* Determine the scheduling policy and the corresponding maximum priority. The latter value is system-dependent, so we have to check it at startup. */ private maxprio; def POL = SCHED_RR, PRIO = maxprio; testprio PRIO = setsched this_thread 0 0 || true where () = setsched this_thread POL PRIO; = false otherwise; maxprio = until (neg testprio) (+1) 1 - 1; /****************************************************************************/ /* Prompt the user to press to stop the given task by sending a stop message. Return the result of the task when finished. */ prompt PROMPT TASK = printf "%s Press to stop.\n" PROMPT || flush || reads || midi_send CTRL PORT stop || result TASK; /****************************************************************************/ /* Generic MIDI input loop. Starting from a given initial state STATE, repeat applying the given function F to the current state and incoming MIDI messages, until we receive a stop message. Return the final STATE when finished. */ input_loop STATE F (_,_,_,stop) = STATE; input_loop STATE F MSG = input_loop (F STATE MSG) F (midi_get APP) otherwise; /* Execute the MIDI input loop as a background job. Make sure the input queue is flushed initially, and that the loop is executed using realtime scheduling. */ input_task STATE F = thread (setsched this_thread POL PRIO || midi_flush APP || input_loop STATE F (midi_get APP)); /****************************************************************************/ /* A simple client doing realtime processing. It outputs an arpeggiated major chord for each note played on the lower half of the keyboard (below middle C). This is done by delaying and shifting the pitch of incoming notes accordingly. Other messages are passed through unchanged. */ private arp; arpeggio = prompt "Arpeggio running." (input_task () arp); arp _ (_,_,TIME,note_on CHAN PITCH VEL) = midi_send APP PORT (TIME,note_on CHAN PITCH VEL) || midi_send APP PORT (TIME+300,note_on CHAN (PITCH+4) VEL) || midi_send APP PORT (TIME+600,note_on CHAN (PITCH+7) VEL) if PITCH < 60; arp _ (_,_,TIME,note_off CHAN PITCH VEL) = midi_send APP PORT (TIME,note_off CHAN PITCH VEL) || midi_send APP PORT (TIME+300,note_off CHAN (PITCH+4) VEL) || midi_send APP PORT (TIME+600,note_off CHAN (PITCH+7) VEL) if PITCH < 60; arp _ (_,_|EV) = midi_send APP PORT EV otherwise; /****************************************************************************/ /* Record a MIDI sequence. Here we simply echo all incoming events and collect them in a list (maintained in the STATE argument). */ private rec; record = prompt "Recording." (input_task [] rec); rec SEQ (_,_|EVENT) = midi_send APP PORT EVENT || append SEQ EVENT; /****************************************************************************/ /* Play back a MIDI sequence. The input is a list of (TIME,MSG) events with absolute timestamps in milliseconds. */ private play1, play2; play SEQ = prompt "Playing." (thread (setsched this_thread POL PRIO || midi_flush APP || play1 emptyset SEQ)); private save_notes, all_notes_off, time_diff; play1 NOTES [] = (); play1 NOTES [(S,MSG)|SEQ] = midi_send APP PORT MSG || play2 (save_notes NOTES MSG) midi_time S SEQ; /* The player loop is invoked with the current playback time T, the current sequence time S, and the remainder of the sequence SEQ. The appropriate delta times are computed on the fly. We also respond to a stop message by turning off sounding notes (cached in the NOTES set) and aborting playback. */ play2 NOTES T S [] = (); play2 NOTES T S SEQ = all_notes_off NOTES if midi_avail APP and then (midi_get APP!3 = stop); play2 NOTES T1 S1 [(S2,MSG)|SEQ] = midi_send APP PORT MSG || play2 (save_notes NOTES MSG) T1 S1 SEQ if S2 = S1; = midi_send APP PORT MSG || play2 (save_notes NOTES MSG) T2 S2 SEQ where T2 = midi_wait APP (time_diff S1 S2+T1); time_diff T1 T2 = ifelse (DT>=0) DT (DT+0x100000000) where DT = T2 mod 0x100000000 - T1 mod 0x100000000; /* Keep track of all sounding notes in a set, to turn them off when playback is interrupted. (There are controllers for that purpose, but, alas, these don't seem to work on all devices.) */ save_notes NOTES (note_on CHAN PITCH VEL) = insert NOTES [CHAN,PITCH] if VEL > 0; = delete NOTES [CHAN,PITCH] otherwise; save_notes NOTES (note_off CHAN PITCH VEL) = delete NOTES [CHAN,PITCH]; save_notes NOTES _ = NOTES otherwise; all_notes_off NOTES = do (\[CHAN,PITCH] . midi_send APP PORT (note_on CHAN PITCH 0)) (members NOTES); /****************************************************************************/ /* Load a sequence from a (type 0 or 1) MIDI file. All tracks are mixed down to a single sequence, meta events are removed, and timestamps are converted to milliseconds, according to the division and the tempo map of the file. The result can be fed directly into the play function for playback. */ /* NOTE: default tempo is 120 BPM = 500000 microsecs per quarter note */ private convert, mix, tracks; load NAME = filter (neg is_meta) (convert (midi_file_division F) 500000 0 0 (foldl mix [] (tracks F))) where F = midi_file_open NAME; /* convert timestamps */ convert _ _ _ _ [] = []; convert (FPS,FRACS) TEMPO T0 MS0 [(T,MSG)|SEQ] = [(MS,MSG)|convert (FPS,FRACS) TEMPO T0 MS0 SEQ] where MS = round (T/(FPS*FRACS)*1000); convert PPQN TEMPO T0 MS0 [(T,tempo TEMPO1)|SEQ] = [(MS,tempo TEMPO1)|convert PPQN TEMPO1 T MS SEQ] where MS = round (MS0+TEMPO/PPQN*(T-T0)/1000); convert PPQN TEMPO T0 MS0 [(T,MSG)|SEQ] = [(MS,MSG)|convert PPQN TEMPO T MS SEQ] where MS = round (MS0+TEMPO/PPQN*(T-T0)/1000); /* mix two tracks */ mix SEQ1 SEQ2 = SEQ1 if null SEQ2; = SEQ2 if null SEQ1; = [hd SEQ1|mix (tl SEQ1) SEQ2] if T1 <= T2 where (T1,_) = hd SEQ1, (T2,_) = hd SEQ2; = [hd SEQ2|mix SEQ1 (tl SEQ2)] otherwise; /* read the tracks from a MIDI file */ tracks F = [midi_file_read_track F : I in [1..midi_file_num_tracks F]];