aboutsummaryrefslogtreecommitdiff
path: root/pd/portmidi/pm_test/latency.c
blob: 87b1965b89e7c260f9e5ae9c6cf7d165efaf8cd5 (plain)
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
/* latency.c -- measure latency of OS */

#include "porttime.h"
#include "portmidi.h"
#include "stdlib.h"
#include "stdio.h"
#include "string.h"
#include "assert.h"

/* Latency is defined here to mean the time starting when a
   process becomes ready to run, and ending when the process
   actually runs. Latency is due to contention for the
   processor, usually due to other processes, OS activity
   including device drivers handling interrupts, and
   waiting for the scheduler to suspend the currently running
   process and activate the one that is waiting.

   Latency can affect PortMidi applications: if a process fails
   to wake up promptly, MIDI input may sit in the input buffer
   waiting to be handled, and MIDI output may not be generated
   with accurate timing. Using the latency parameter when 
   opening a MIDI output port allows the caller to defer timing
   to PortMidi, which in most implementations will pass the
   data on to the OS. By passing timestamps and data to the
   OS kernel, device driver, or even hardware, there are fewer
   sources of latency that can affect the ultimate timing of
   the data. On the other hand, the application must generate
   and deliver the data ahead of the timestamp. The amount by 
   which data is computed early must be at least as large as
   the worst-case latency to avoid timing problems.

   Latency is even more important in audio applications. If an
   application lets an audio output buffer underflow, an audible
   pop or click is produced. Audio input buffers can overflow,
   causing data to be lost. In general the audio buffers must
   be large enough to buffer the worst-case latency that the
   application will encounter.

   This program measures latency by recording the difference
   between the scheduled callback time and the current real time.
   We do not really know the scheduled callback time, so we will
   record the differences between the real time of each callback
   and the real time of the previous callback. Differences that
   are larger than the scheduled difference are recorded. Smaller
   differences indicate the system is recovering from an earlier
   latency, so these are ignored.
   Since printing by the callback process can cause all sorts of
   delays, this program records latency observations in a
   histogram. When the program is stopped, the histogram is
   printed to the console.

   Optionally the system can be tested under a load of MIDI input,
   MIDI output, or both.  If MIDI input is selected, the callback
   thread will read any waiting MIDI events each iteration.  You
   must generate events on this interface for the test to actually
   put any appreciable load on PortMidi.  If MIDI output is
   selected, alternating note on and note off events are sent each
   X iterations, where you specify X.  For example, with a timer
   callback period of 2ms and X=1, a MIDI event is sent every 2ms.


   INTERPRETING RESULTS: Time is quantized to 1ms, so there is
   some uncertainty due to rounding. A microsecond latency that
   spans the time when the clock is incremented will be reported
   as a latency of 1. On the other hand, a latency of almost
   1ms that falls between two clock ticks will be reported as 
   zero. In general, if the highest nonzero bin is numbered N,
   then the maximum latency is N+1.

CHANGE LOG

18-Jul-03 Mark Nelson -- Added code to generate MIDI or receive
            MIDI during test, and made period user-settable.
 */

#define HIST_LEN 21 /* how many 1ms bins in the histogram */

#define STRING_MAX 80 /* used for console input */

#define INPUT_BUFFER_SIZE 100
#define OUTPUT_BUFFER_SIZE 0

#ifndef max
#define max(a, b) ((a) > (b) ? (a) : (b))
#endif
#ifndef min
#define min(a, b) ((a) <= (b) ? (a) : (b))
#endif

int get_number(char *prompt);

PtTimestamp previous_callback_time = 0;

int period;            /* milliseconds per callback */

long histogram[HIST_LEN];
long max_latency = 0;  /* worst latency observed */
long out_of_range = 0; /* how many points outside of HIST_LEN? */

int test_in, test_out; /* test MIDI in and/or out? */
int output_period;     /* output MIDI every __ iterations if test_out true */
int iteration = 0;
PmStream *in, *out;
int note_on = 0;       /* is the note currently on? */

/* callback function for PortTime -- computes histogram */
void pt_callback(PtTimestamp timestamp, void *userData)
{
    PtTimestamp difference = timestamp - previous_callback_time - period;
    previous_callback_time = timestamp;

    /* allow 5 seconds for the system to settle down */
    if (timestamp < 5000) return;

    iteration++;
    /* send a note on/off if user requested it */
    if (test_out && (iteration % output_period == 0)) {
        PmEvent buffer[1];
        buffer[0].timestamp = Pt_Time(NULL);
        if (note_on) {
            /* note off */
            buffer[0].message = Pm_Message(0x90, 60, 0);
            note_on = 0;
        } else {
            /* note on */
            buffer[0].message = Pm_Message(0x90, 60, 100);
            note_on = 1;
        }
        Pm_Write(out, buffer, 1);
        iteration = 0;
    }

    /* read all waiting events (if user requested) */
    if (test_in) {
       PmError status;
       PmEvent buffer[1];
       do {
          status = Pm_Poll(in);
          if (status == TRUE) {
              Pm_Read(in,buffer,1);
          }
       } while (status == TRUE);
    }

    if (difference < 0) return; /* ignore when system is "catching up" */

    /* update the histogram */
    if (difference < HIST_LEN) {
        histogram[difference]++;
    } else {
        out_of_range++;
    }

    if (max_latency < difference) max_latency = difference;
}


int main()
{
    char line[STRING_MAX];
    int i;
    int len;
    int choice;
    PtTimestamp stop;
    printf("Latency histogram.\n");
    period = get_number("Choose timer period (in ms): ");
    assert(period >= 1);
    printf("Benchmark with:\n\t%s\n\t%s\n\t%s\n\t%s\n",
           "1. No MIDI traffic",
           "2. MIDI input",
           "3. MIDI output",
           "4. MIDI input and output");
    choice = get_number("? ");
    switch (choice) {
      case 1: test_in = 0; test_out = 0; break;
      case 2: test_in = 1; test_out = 0; break;
      case 3: test_in = 0; test_out = 1; break;
      case 4: test_in = 1; test_out = 1; break;
      default: assert(0);
    }
    if (test_in || test_out) {
        /* list device information */
        for (i = 0; i < Pm_CountDevices(); i++) {
            const PmDeviceInfo *info = Pm_GetDeviceInfo(i);
            if ((test_in && info->input) ||
                (test_out && info->output)) {
                printf("%d: %s, %s", i, info->interf, info->name);
                if (info->input) printf(" (input)");
                if (info->output) printf(" (output)");
                printf("\n");
            }
        }
        /* open stream(s) */
        if (test_in) {
            int i = get_number("MIDI input device number: ");
            Pm_OpenInput(&in, 
                  i,
                  NULL, 
                  INPUT_BUFFER_SIZE, 
                  (long (*)(void *)) Pt_Time, 
                  NULL);
            /* turn on filtering; otherwise, input might overflow in the 
               5-second period before timer callback starts reading midi */
            Pm_SetFilter(in, PM_FILT_ACTIVE | PM_FILT_CLOCK);
        }
        if (test_out) {
            int i = get_number("MIDI output device number: ");
            PmEvent buffer[1];
            Pm_OpenOutput(&out, 
                  i,
                  NULL,
                  OUTPUT_BUFFER_SIZE,
                  (long (*)(void *)) Pt_Time,
                  NULL, 
                  0); /* no latency scheduling */

            /* send a program change to force a status byte -- this fixes
               a problem with a buggy linux MidiSport driver, and shouldn't
               hurt anything else
             */
            buffer[0].timestamp = 0;
            buffer[0].message = Pm_Message(0xC0, 0, 0); /* program change */
            Pm_Write(out, buffer, 1);

            output_period = get_number(
                "MIDI out should be sent every __ callback iterations: ");

            assert(output_period >= 1);
        }
    }

    printf("%s%s", "Latency measurements will start in 5 seconds. ",
                   "Type return to stop: ");
    Pt_Start(period, &pt_callback, 0);
    fgets(line, STRING_MAX, stdin);
    stop = Pt_Time();
    Pt_Stop();

    /* courteously turn off the last note, if necessary */
    if (note_on) {
       PmEvent buffer[1];
       buffer[0].timestamp = Pt_Time(NULL);
       buffer[0].message = Pm_Message(0x90, 60, 0);
       Pm_Write(out, buffer, 1);
    }

    /* print the histogram */
    printf("Duration of test: %g seconds\n\n", max(0, stop - 5000) * 0.001);
    printf("Latency(ms)  Number of occurrences\n");
    /* avoid printing beyond last non-zero histogram entry */
    len = min(HIST_LEN, max_latency + 1);
    for (i = 0; i < len; i++) {
        printf("%2d      %10ld\n", i, histogram[i]);
    }
    printf("Number of points greater than %dms: %ld\n", 
           HIST_LEN - 1, out_of_range);
    printf("Maximum latency: %ld milliseconds\n", max_latency);
    printf("\nNote that due to rounding, actual latency can be 1ms higher\n");
    printf("than the numbers reported here.\n");
    printf("Type return to exit...");
    fgets(line, STRING_MAX, stdin);
    return 0;
}


/* read a number from console */
int get_number(char *prompt)
{
    char line[STRING_MAX];
    int n = 0, i;
    printf(prompt);
    while (n != 1) {
        n = scanf("%d", &i);
        fgets(line, STRING_MAX, stdin);

    }
    return i;
}