3

In order to write a MIDI sequencer I need a steady pulse that calls a timing routine that has absolute priority over anything else in the program and preferrably over anything in the computer. I do this by using TimeSetEvent like this:

TimeSetEvent (FInterval, 0, TimerUpdate, uInt32 (Self), TIME_PERIODIC);

where TimerUpdate is a callback that resumes a separate thread with priority tpTimeCritical and that one calls a routine (FOnTimer) in which to handle all MIDI events.

procedure TThreaded_Timer.Execute;
begin
   if Assigned (FOnTimer) then
   begin
      while not Terminated do
      begin
         FOnTimer (Self);
         if not Terminated then Suspend;
      end; // while
   end; // if
   Terminate;
end; // Execute //

Although this construction is much better than some things I tried before it is still very sensitive to some events. To my surprise it stutters at each display of a hint. Why can a simple hint cause such an interruption of a time critical thread? Of course I can switch it off but which nasty surprises are still waiting for me?

Robert Harvey
  • 178,213
  • 47
  • 333
  • 501
Arnold
  • 4,578
  • 6
  • 52
  • 91
  • 1
    Suspend/Resume is not a good idea. See here for more information [tthread-resume-is-deprecated-in-delphi-2010-what-should-be-used-in-plac](http://stackoverflow.com/questions/1418333/tthread-resume-is-deprecated-in-delphi-2010-what-should-be-used-in-place) – LU RD Sep 24 '11 at 17:51
  • What time resolution do you need? – David Heffernan Sep 24 '11 at 18:30
  • 1
    You should use multimedia timers for that purpose, there is no point in reimplementing them by youself (yes, deeply below this API is threads with `spin-locks`) – Premature Optimization Sep 24 '11 at 18:52
  • I know about deprecating resume/suspend, but it still works and imho the way i use it hear does not conflict with the problems mentioned in the article. Resolution is 16, but a resolution of 1 or 0 does make no difference. Multimedia timers I used as well, but they are no improvement over this timer. – Arnold Sep 24 '11 at 20:50
  • @Arnold, not entirely correct. Doc says : Warning: The Resume and Suspend methods should only be used for debugging purposes. Suspending a thread using Suspend can lead to deadlocks and undefined behavior within your application. Proper thread synchronization techniques should be based on TEvent and TMutex. – LU RD Sep 24 '11 at 20:58
  • You're doing a disservice by making the callback trigger code to execute in a THREAD_PRIORITY_TIME_CRITICAL thread. That's because the multimedia timer already runs in a thread of its own having THREAD_PRIORITY_TIME_CRITICAL priority. In the end, you're decreasing the relative priority of the timer thread. What matters is how accurate the timer fires, not how quick the callback executes in response to an already delayed timer. ... – Sertac Akyuz Sep 24 '11 at 23:44
  • ... But in any case, don't bother too much, you'll probably not be able to get what you want from the mm timer. The little I had browsed midi developers' forums when I was reading about timers, what I learned was that they're using external hardware for serious timing. – Sertac Akyuz Sep 24 '11 at 23:45
  • @Sertac, I use a lot of sequencers that do a much better job of timing than my code does on my own computer without specific hardware. They do something I don't. I am just curious to know. Second: why does showing a hint (and maybe something else) disrupt the timing process that much? – Arnold Sep 25 '11 at 06:28
  • @LU RD, as far as I understand the trouble with suspend/resume is that you can disrupt a thread when suspending it from outside, one can't be sure what exactly is suspended. That seems to be the reason for it deprecation [http://msdn.microsoft.com/en-us/library/system.threading.thread.suspend.aspx]. I don't see why not using it when used inside a thread or when you are sure that you resume a thread already being suspended. – Arnold Sep 25 '11 at 06:41

4 Answers4

5

Use the multimedia timer which is designed for this purpose. Delphi timers are awful and really only get attention during idle time. A thread-based timer is only useful when that thread gets attention. The MMTimer operates at kernel level and provides a callback that is really quite accutate. We use it for hardware sequencing automation control it is so good.

Here is my unit that implements an MMTimer as a more easy to use TTimer. Use 'Repeat' to make it single-shot or repetetive.

unit UArtMMTimer;

interface

uses
  Classes,
  SysUtils,
  ExtCtrls,
  MMSystem;

type
  TArtMMTimer = class( TObject )
    constructor Create;
    destructor  Destroy; override;
  PRIVATE
    FHandle              : MMResult;
    FRepeat              : boolean;
    FIntervalMS          : integer;
    FOnTimer             : TNotifyEvent;
    FEnabled             : boolean;
    procedure   RemoveEvent;
    procedure   InstallEvent;
    procedure   DoOnCallback;
    procedure   SetEnabled( AState : boolean );
    procedure   SetIntervalMS( AValue : integer );
  PUBLIC
    property  Enabled : boolean
                read FEnabled
                write SetEnabled;
    property  OnTimer : TNotifyEvent
                read FOnTimer
                write FOnTimer;
    property  IntervalMS : integer
                read FIntervalMS
                write SetIntervalMS;
  end;



implementation

uses
  Windows;


// TArtMMTimer
// --------------------------------------------------------------------


procedure MMTCallBack(uTimerID, uMessage: UINT;
    dwUser, dw1, dw2: DWORD) stdcall;
var
  Timer : TArtMMTimer;
begin
  Timer := TArtMMTimer( dwUser );
  Timer.DoOnCallback;
end;



constructor TArtMMTimer.Create;
begin
  Inherited Create;
  FIntervalMS := 100;
  FRepeat     := True;
end;


destructor  TArtMMTimer.Destroy;
begin
  FOnTimer := nil;
  RemoveEvent;
  Inherited Destroy;
end;


procedure   TArtMMTimer.RemoveEvent;
begin
  If FHandle <> 0 then
    begin
    timeKillEvent( FHandle );
    FHandle := 0;
    end;

end;

procedure   TArtMMTimer.InstallEvent;
var
  iFlags : integer;
begin
  RemoveEvent;

  If FRepeat then
    iFlags := TIME_PERIODIC Or TIME_CALLBACK_FUNCTION
   else
    iFlags := TIME_CALLBACK_FUNCTION;

  FHandle := timeSetEvent(
    FIntervalMS,
    0,
    @MMTCallBack,
    DWord(Self),
    iFlags );
end;

procedure   TArtMMTimer.SetEnabled( AState : boolean );
begin
  If AState <> FEnabled then
    begin
    FEnabled := AState;
    If FEnabled then
      InstallEvent
     else
      RemoveEvent;
    end;
end;



procedure   TArtMMTimer.DoOnCallback;
var
  NowHRCount, WaitHRCount,IntervalHRCount : THRCount;
begin
  If Assigned( FOnTimer ) then
    FOnTimer( Self );
end;


procedure   TArtMMTimer.SetIntervalMS( AValue : integer );
begin
  If AValue <> FIntervalMS then
    begin
    FIntervalMS := AValue;
    If Enabled then
      begin
      Enabled := False;
      Enabled := True;
      end;
    end;
end;

// End TArtMMTimer
// --------------------------------------------------------------------










end.
Brian Frost
  • 13,334
  • 11
  • 80
  • 154
  • Thanks for your code example, I will try this out as well. I have now three suggestions, thank you all very much! I will implement and test these and compare the results with my own code. I'll report my findings here. That will not be before Monday. – Arnold Sep 25 '11 at 06:37
  • 2
    I don't understand, Arnold is already using a multimedia timer. His code is calling 'timeSetEvent', that's in the question. – Sertac Akyuz Sep 25 '11 at 09:58
1

The accuracy of the multimedia timers is not that great.Here is an article that explains why.

Instead of relying on a timer to wake up your thread why don't you manage your sleep and wake times all within the thread itself?

Maybe something like this (in pseudo-code, sorry I don't know Delphi):

my_critical_thread()
{
    while (true) {
        time = get_current_time()
        do_work();
        time = interval - (get_current_time() - time)
        if (time > 0)
            sleep(time)
    }
}

This should get you very close to your target interval if the thread is set to critical priority, assuming the work you do on each iteration leaves time for your other threads and the rest of the system to do their thing.

Good luck.

Miguel Grinberg
  • 65,299
  • 14
  • 133
  • 152
  • I have considered this solution, but until now rejected it because it seems sensitive for workloads that are over time and so be sensitive for processor speeds. I will implement this solution now you suggest it. – Arnold Sep 24 '11 at 21:01
  • The TStopWatch may be the proper tool for this job. – LU RD Sep 24 '11 at 21:30
  • @Arnold: out of curiosity, what is the interval that you need for your thread? – Miguel Grinberg Sep 25 '11 at 06:40
  • With bpm=120 I use Interval=22. Changing the bpm changes the the Interval proportionally. – Arnold Sep 26 '11 at 18:57
  • My understanding is that on Windows things get really funky when you try to get sub-15ms, so on that account you should be fine. You should time your worker thread, the work that you do there must be extremely light for things to work at that high rate. – Miguel Grinberg Sep 27 '11 at 05:54
  • In my timer experiments (see answer below) this was the most precise way of timing. I did not use sleep, but used a loop with the queryperformance counter. When I used asm pause; end the timer stuttered, with application.processmessages it worked great. With the queryperformancecounter you can get timings below the 15 ms. The most insensitive for the workload is the timerforget class. – Arnold Oct 13 '11 at 20:01
1

Set a timer to a slightly shorter time than required (10 ms less, for example).

When the timer occurs, raise thread priority to "above normal".

Calculate the remaining time to wait and execute Sleep with a slightly shorter interval (1 ms less, for example).

Now start waiting in a loop for the correct time. In each loop occurrence execute at least one asm pause; end; instruction to not push a core to the 100% usage.

When the time occurs, lower thread priority to "normal".

I think that's the best you can do without writing a device driver.

gabr
  • 26,580
  • 9
  • 75
  • 141
1

Thanks for all the suggestions. In order to test them I developed a small Delphi program in order to test the suggested algorithms. Four algorithms are tested

  • Simple (by Brian Frost) - Uses the Multimedia timer (TimeSetEvent), which calls a callback that performs the timed task.
  • Threaded (by Brian Pedersen) - As with simple, but the callback is called in a separate thread. The thread receives the highest priority possible (TimeCritical).
  • Looping (by Miguel) - In this algorithm we don't trust the timer at all and write one ourselves. The callback is performed in a loop, after each call we examine how much time is still left until the next tick and wait until the next has to take place. The thread has highest priority. I used the asm pause; end suggestion from Gabr as a fast way of event processing.
  • Fire & Forget (by myself) - The multimedia timer at each tick creates a separate thread with the highest priority, assigns it the callback and forgets about it. This has the advantage that even when a previous thread has not yet finished, a new one can start already and if you are lucky - on a new processor.

You can find the results here. Most timers run correct with a normal workload, though the simple multimedia timer shows the most variability. The looping timer is the most precise. All timers except the fire & forget run into problems when the workload results in work that lasts longer than the Interval. The best performance comes from the fire & forget algorithm. However, one should take care that no shared resources are used in the callback because the callback can be invoked multiple times when the workload increases. In fact in the current implementation MIDIOut can be invoked simultaneously so it should be surrounded by a critical section.

When another programs is run the timers show a greater variability. The looping timer still performs best.

When I add the timer to my original MIDI sequencer the original question remains als. Hints keep interrupting the timer as before though they did not so in the test version which you can download.

                  Parameters                            Constancy of    Beat                Workload
Timer              N Interval Resolution WorkLoad        Mean     s.d.     Min     Max    Mean    s.d.     Min     Max
Simple           226       22         30     1000      22.001    0.001  21.996  22.009   0.093   0.036   0.079   0.302
Threaded         226       22         30     1000      22.001    0.004  21.964  22.031   0.091   0.032   0.079   0.253
Looping          227       22         30     1000      22.000    0.002  21.999  22.025   0.093   0.034   0.079   0.197
Fire & Forget    226       22         30     1000      22.001    0.008  21.964  22.042   0.091   0.031   0.079   0.186
Simple           226       22         15     1000      22.001    0.002  21.989  22.011   0.091   0.031   0.079   0.224
Threaded         226       22         15     1000      22.001    0.003  21.978  22.031   0.091   0.032   0.079   0.185
Looping          227       22         15     1000      22.000    0.001  21.999  22.015   0.092   0.034   0.079   0.209
Fire & Forget    226       22         15     1000      22.001    0.015  21.861  22.146   0.091   0.031   0.079   0.173
Simple           226       22          0     1000      22.001    0.001  21.997  22.005   0.091   0.030   0.079   0.190
Threaded         226       22          0     1000      22.001    0.003  21.979  22.029   0.091   0.031   0.079   0.182
Looping          227       22          0     1000      22.000    0.000  21.999  22.002   0.092   0.034   0.079   0.194
Fire & Forget    226       22          0     1000      22.001    0.026  21.747  22.256   0.090   0.030   0.079   0.180
Simple           226       22         30    10000      22.001    0.002  21.992  22.012   0.801   0.034   0.787   1.001
Threaded         226       22         30    10000      22.001    0.002  21.994  22.008   0.800   0.031   0.787   0.898
Looping          227       22         30    10000      22.000    0.000  21.999  22.000   0.802   0.034   0.787   0.919
Fire & Forget    226       22         30    10000      22.001    0.010  21.952  22.087   0.903   0.230   0.788   1.551
Simple           226       22         15    10000      22.001    0.002  21.984  22.020   0.810   0.081   0.788   1.417
Threaded         226       22         15    10000      22.001    0.006  21.981  22.073   0.800   0.031   0.788   0.889
Looping          227       22         15    10000      22.000    0.000  21.999  22.000   0.802   0.036   0.787   0.969
Fire & Forget    226       22         15    10000      22.001    0.009  21.914  22.055   0.799   0.030   0.788   0.885
Simple           226       22          0    10000      22.001    0.002  21.994  22.006   0.799   0.030   0.788   0.894
Threaded         226       22          0    10000      22.001    0.005  21.953  22.048   0.799   0.030   0.787   0.890
Looping          227       22          0    10000      22.000    0.000  21.999  22.002   0.801   0.034   0.787   0.954
Fire & Forget    226       22          0    10000      22.001    0.007  21.977  22.029   0.799   0.030   0.788   0.891
Simple           226       22         30   100000      22.001    0.002  21.988  22.017   7.900   0.052   7.879   8.289
Threaded         226       22         30   100000      22.001    0.003  21.967  22.035   7.897   0.036   7.879   8.185
Looping          227       22         30   100000      22.000    0.001  21.999  22.015   7.908   0.098   7.879   9.165
Fire & Forget    225       22         30   100000      22.001    0.007  21.960  22.027   7.901   0.038   7.880   8.061
Simple           227       22         15   100000      22.014    0.195  21.996  24.934   7.902   0.056   7.879   8.351
Threaded         226       22         15   100000      22.001    0.002  21.997  22.008   7.900   0.049   7.879   8.362
Looping          227       22         15   100000      22.000    0.000  22.000  22.000   7.900   0.046   7.879   8.229
Fire & Forget    225       22         15   100000      22.001    0.008  21.962  22.065   7.906   0.082   7.880   8.891
Simple           227       22          0   100000      22.018    0.261  21.937  25.936   7.901   0.050   7.879   8.239
Threaded         226       22          0   100000      22.001    0.001  21.998  22.005   7.897   0.031   7.879   7.987
Looping          227       22          0   100000      22.000    0.000  21.999  22.000   7.901   0.053   7.879   8.263
Fire & Forget    225       22          0   100000      22.001    0.007  21.967  22.032   7.900   0.044   7.880   8.308
Simple            63       22         30  1000000      78.027    6.801  24.938  80.730  77.754   8.947   7.890  80.726
Threaded          56       22         30  1000000      87.908    1.334  78.832  91.787  78.897   0.219  78.819  80.430
Looping           62       22         30  1000000      78.923    0.320  78.808  80.749  78.923   0.320  78.808  80.748
Fire & Forget    222       22         30  1000000      22.001    0.009  21.956  22.038  84.212   3.431  78.825  91.812
Simple            66       22         15  1000000      75.656   13.090  21.994  80.714  79.183   1.559  78.811  90.950
Threaded          56       22         15  1000000      87.841    1.204  78.991  88.011  78.849   0.043  78.812  79.003
Looping           62       22         15  1000000      78.880    0.207  78.807  80.442  78.880   0.207  78.807  80.441
Fire & Forget    222       22         15  1000000      22.001    0.978  11.975  32.042  84.915   3.569  78.816  90.917
Simple            66       22          0  1000000      75.681   12.992  21.991  80.778  79.213   1.400  78.807  87.766
Threaded          56       22          0  1000000      87.868    1.238  78.889  89.515  78.954   0.597  78.813  83.164
Looping           62       22          0  1000000      78.942    0.307  78.806  80.380  78.942   0.307  78.806  80.379
Fire & Forget    222       22          0  1000000      22.001    0.011  21.926  22.076  83.953   3.103  78.821  91.145
Arnold
  • 4,578
  • 6
  • 52
  • 91