midiclock.c (8916B)
1 /* miditime.c -- a test program that sends midi clock and MTC */ 2 3 #include "portmidi.h" 4 #include "porttime.h" 5 #include <stdlib.h> 6 #include <stdio.h> 7 #include <string.h> 8 #include <assert.h> 9 #include <ctype.h> 10 11 #ifndef false 12 #define false 0 13 #define true 1 14 #endif 15 16 #define private static 17 typedef int boolean; 18 19 #define MIDI_TIME_CLOCK 0xf8 20 #define MIDI_START 0xfa 21 #define MIDI_CONTINUE 0xfb 22 #define MIDI_STOP 0xfc 23 #define MIDI_Q_FRAME 0xf1 24 25 #define OUTPUT_BUFFER_SIZE 0 26 #define DRIVER_INFO NULL 27 #define TIME_PROC ((int32_t (*)(void *)) Pt_Time) 28 #define TIME_INFO NULL 29 #define LATENCY 0 30 #define TIME_START Pt_Start(1, 0, 0) /* timer started w/millisecond accuracy */ 31 32 #define STRING_MAX 80 /* used for console input */ 33 34 /* to determine ms per clock: 35 * time per beat in seconds = 60 / tempo 36 * multiply by 1000 to get time per beat in ms: 60000 / tempo 37 * divide by 24 CLOCKs per beat: (60000/24) / tempo 38 * simplify: 2500 / tempo 39 */ 40 #define TEMPO_TO_CLOCK 2500.0 41 42 boolean done = false; 43 PmStream *midi; 44 /* shared flags to control callback output generation: */ 45 boolean clock_running = false; 46 boolean send_start_stop = false; 47 boolean time_code_running = false; 48 boolean active = false; /* tells callback to do its thing */ 49 float tempo = 60.0F; 50 /* protocol for handing off portmidi to callback thread: 51 main owns portmidi 52 main sets active = true: ownership transfers to callback 53 main sets active = false: main requests ownership 54 callback sees active == false, yields ownership back to main 55 main waits 2ms to make sure callback has a chance to yield 56 (stop making PortMidi calls), then assumes it can close 57 PortMidi 58 */ 59 60 /* timer_poll -- the timer callback function */ 61 /* 62 * All MIDI sends take place here 63 */ 64 void timer_poll(PtTimestamp timestamp, void *userData) 65 { 66 static int callback_owns_portmidi = false; 67 static PmTimestamp clock_start_time = 0; 68 static double next_clock_time = 0; 69 /* SMPTE time */ 70 static int frames = 0; 71 static int seconds = 0; 72 static int minutes = 0; 73 static int hours = 0; 74 static int mtc_count = 0; /* where are we in quarter frame sequence? */ 75 static int smpte_start_time = 0; 76 static double next_smpte_time = 0; 77 #define QUARTER_FRAME_PERIOD (1.0 / 120.0) /* 30fps, 1/4 frame */ 78 79 if (callback_owns_portmidi && !active) { 80 /* main is requesting (by setting active to false) that we shut down */ 81 callback_owns_portmidi = false; 82 return; 83 } 84 if (!active) return; /* main still getting ready or it's closing down */ 85 callback_owns_portmidi = true; /* main is ready, we have portmidi */ 86 if (send_start_stop) { 87 if (clock_running) { 88 Pm_WriteShort(midi, 0, MIDI_STOP); 89 } else { 90 Pm_WriteShort(midi, 0, MIDI_START); 91 clock_start_time = timestamp; 92 next_clock_time = TEMPO_TO_CLOCK / tempo; 93 } 94 clock_running = !clock_running; 95 send_start_stop = false; /* until main sets it again */ 96 /* note that there's a slight race condition here: main could 97 set send_start_stop asynchronously, but we assume user is 98 typing slower than the clock rate */ 99 } 100 if (clock_running) { 101 if ((timestamp - clock_start_time) > next_clock_time) { 102 Pm_WriteShort(midi, 0, MIDI_TIME_CLOCK); 103 next_clock_time += TEMPO_TO_CLOCK / tempo; 104 } 105 } 106 if (time_code_running) { 107 int data = 0; // initialization avoids compiler warning 108 if ((timestamp - smpte_start_time) < next_smpte_time) 109 return; 110 switch (mtc_count) { 111 case 0: /* frames low nibble */ 112 data = frames; 113 break; 114 case 1: /* frames high nibble */ 115 data = frames >> 4; 116 break; 117 case 2: /* frames seconds low nibble */ 118 data = seconds; 119 break; 120 case 3: /* frames seconds high nibble */ 121 data = seconds >> 4; 122 break; 123 case 4: /* frames minutes low nibble */ 124 data = minutes; 125 break; 126 case 5: /* frames minutes high nibble */ 127 data = minutes >> 4; 128 break; 129 case 6: /* hours low nibble */ 130 data = hours; 131 break; 132 case 7: /* hours high nibble */ 133 data = hours >> 4; 134 break; 135 } 136 data &= 0xF; /* take only 4 bits */ 137 Pm_WriteShort(midi, 0, 138 Pm_Message(MIDI_Q_FRAME, (mtc_count << 4) + data, 0)); 139 mtc_count = (mtc_count + 1) & 7; /* wrap around */ 140 if (mtc_count == 0) { /* update time by two frames */ 141 frames += 2; 142 if (frames >= 30) { 143 frames = 0; 144 seconds++; 145 if (seconds >= 60) { 146 seconds = 0; 147 minutes++; 148 if (minutes >= 60) { 149 minutes = 0; 150 hours++; 151 /* just let hours wrap if it gets that far */ 152 } 153 } 154 } 155 } 156 next_smpte_time += QUARTER_FRAME_PERIOD; 157 } else { /* time_code_running is false */ 158 smpte_start_time = timestamp; 159 /* so that when it finally starts, we'll be in sync */ 160 } 161 } 162 163 164 /* read a number from console */ 165 /**/ 166 int get_number(char *prompt) 167 { 168 char line[STRING_MAX]; 169 int n = 0, i; 170 printf(prompt); 171 while (n != 1) { 172 n = scanf("%d", &i); 173 fgets(line, STRING_MAX, stdin); 174 175 } 176 return i; 177 } 178 179 /**************************************************************************** 180 * showhelp 181 * Effect: print help text 182 ****************************************************************************/ 183 184 private void showhelp() 185 { 186 printf("\n"); 187 printf("t toggles sending MIDI Time Code (MTC)\n"); 188 printf("c toggles sending MIDI CLOCK (initially on)\n"); 189 printf("m to set tempo (from 1bpm to 300bpm)\n"); 190 printf("q quits\n"); 191 printf("\n"); 192 } 193 194 /**************************************************************************** 195 * doascii 196 * Inputs: 197 * char c: input character 198 * Effect: interpret to control output 199 ****************************************************************************/ 200 201 private void doascii(char c) 202 { 203 if (isupper(c)) c = tolower(c); 204 if (c == 'q') done = true; 205 else if (c == 'c') { 206 printf("%s MIDI CLOCKs\n", (clock_running ? "Stopping" : "Starting")); 207 send_start_stop = true; 208 } else if (c == 't') { 209 printf("%s MIDI Time Code\n", 210 (time_code_running ? "Stopping" : "Starting")); 211 time_code_running = !time_code_running; 212 } else if (c == 'm') { 213 int input_tempo = get_number("Enter new tempo (bpm): "); 214 if (input_tempo >= 1 && input_tempo <= 300) { 215 printf("Changing tempo to %d\n", input_tempo); 216 tempo = (float) input_tempo; 217 } else { 218 printf("Tempo range is 1 to 300, current tempo is %g bpm\n", 219 tempo); 220 } 221 } else { 222 showhelp(); 223 } 224 } 225 226 227 /* main - prompt for parameters, start processing */ 228 /* 229 * Prompt user to type return. 230 * Then send START and MIDI CLOCK for 60 beats/min. 231 * Commands: 232 * t - toggle sending MIDI Time Code (MTC) 233 * c - toggle sending MIDI CLOCK 234 * m - set tempo 235 * q - quit 236 */ 237 int main(int argc, char **argv) 238 { 239 char s[STRING_MAX]; /* console input */ 240 int outp; 241 PmError err; 242 int i; 243 if (argc > 1) { 244 printf("Warning: command line arguments ignored\n"); 245 } 246 showhelp(); 247 /* use porttime callback to send midi */ 248 Pt_Start(1, timer_poll, 0); 249 /* list device information */ 250 printf("MIDI output devices:\n"); 251 for (i = 0; i < Pm_CountDevices(); i++) { 252 const PmDeviceInfo *info = Pm_GetDeviceInfo(i); 253 if (info->output) printf("%d: %s, %s\n", i, info->interf, info->name); 254 } 255 outp = get_number("Type output device number: "); 256 err = Pm_OpenOutput(&midi, outp, DRIVER_INFO, OUTPUT_BUFFER_SIZE, 257 TIME_PROC, TIME_INFO, LATENCY); 258 if (err) { 259 printf(Pm_GetErrorText(err)); 260 goto error_exit_no_device; 261 } 262 active = true; 263 264 printf("Type RETURN to start MIDI CLOCK:\n"); 265 if (!fgets(s, STRING_MAX, stdin)) goto error_exit; 266 send_start_stop = true; /* send START and then CLOCKs */ 267 268 while (!done) { 269 if (fgets(s, STRING_MAX, stdin)) { 270 doascii(s[0]); 271 } 272 } 273 274 error_exit: 275 active = false; 276 Pt_Sleep(2); /* this is to allow callback to complete -- it's 277 real time, so it's either ok and it runs on 278 time, or there's no point to synchronizing 279 with it */ 280 /* now we "own" portmidi again */ 281 Pm_Close(midi); 282 error_exit_no_device: 283 Pt_Stop(); 284 Pm_Terminate(); 285 exit(0); 286 } 287