High Resolution Multimedia Timer
A Stable, Safe high resolution timer for VB applications, capable of 1ms resolution.
The standard timer supplied with VB is great for most tasks, but the frequency it updates
at isn't acceptable for high-performance multimedia. In audio applications the system must be
capable of firing audio events with a 1ms resolution, otherwise the ear will be able to
discern the timing inaccuracy.
This article presents a small, hardcore multimedia timer capable of 1ms resolution in VB code.
Timer Overview
The VB timer is based on the standard Win32 timer, and this is not intended for time critical tasks.
The basic minimum resolution of this timer is no better than about 50ms on a Pentium II system
running Win9x, although it is somewhat better on NT/2000/XP, at around 10ms.
This leads to an irritating VB coding problem: if you write graphics code on an NT system
using a timer it all seems to work nicely, but when you run it on Win9x it runs too slowly because
the timer doesn't fire so quickly. Whilst one alternative to this sort of problem is using a
DoEvents, Sleep and QueryPerformanceCounter loop, this is rarely what you want in
a real world application.
About Multimedia Timers
Multimedia timers are implemented using the Win32 multimedia library (winmm.dll). Before
creating a timer, it is important to be aware of a number of system limitations and
VB problems which can occur when using multimedia timers.
Timers are a Limited Resource
In Win9x, the system is limited to a maximum of only 32 multimedia timers.
(In NT, things are much better and each process on the system can have up to 16 timers.) To
provide full Win9x support it is therefore desirable to minimise the number of timers
you create to reduce the chance of running out of timer resources. A single application should
never create more than one timer regardless of how many controls, DLLs or modules within that
application require the timer's services.
Multimedia Timers are remorseless
If you build a multimedia timer directly into a VB application, you will almost certainly
not be able to debug it with any consistency. Building this timer component was somewhat
tricky for this reason! Any time the application stops for an error or a break-point,
the timer keeps on firing from its own separate thread in the background. This
immediately crashes or locks up the VB IDE. Adding Debug.Print messages doesn't help
either - the debug window can't keep up with the high rate of messages fired by the timer and
VB IDE locks up in a loop again.
You can only make a handful of calls from the timer event
The Win32 SDK states that only the following calls are allowed during a multimedia timer event:
- PostMessage
- timeGetSystemTime
- timeGetTime
- timeSetEvent
- timeKillEvent
- midiOutShortMsg
- midiOutLongMsg
- OutputDebugString
This isn't much! It seems to be correct for most standard operations. For example, I found
just putting a Debug.Print statement in the timer event causes the timer to crash when
you try to terminate it. Putting methods there to raise an event directly also caused an
untimely crash. (Note that from other MSDN code, it would appear that DirectX calls are allowed)
Implementing a Stable HiRes Timer in VB
The first thing to do with this sort of component is to ensure the code can be isolated
from the VB IDE by coding it into an ActiveX DLL. You really don't want to have to try to
deal with the IDE locking up every time you set a break-point.
Once it is in the DLL then the next thing is to consider how to reduce the number of
timer instances to ensure you aren't too likely to run out of them. In this code, the timer
itself is implemented in a module. If you implement code in a module, this module will be global
to all classes which use it within a process. As well as your executable project, this includes
any OCX or DLL which attaches to the DLL. Therefore it is possible to create a single timer to
service as many classes as you want. The trick is to ensure that the module doesn't get a
reference to the classes that are attached, otherwise you will get a circular reference.
You can prevent circular references by storing object pointers rather than direct references to
classes as described in the article Subclassing without the crashes.
The final problem to work around is that you can only call a very limited selection of
functions during a multimedia timer event. I work around this by using the PostMessage
function. The function posts a custom WM_USER message to a hidden form owned by the
timer module within the DLL. The form is subclassed by the DLL so when the custom message is
received it is intercepted by the hidden form's WindowProc function, and this allows
the module to forward the message on as an event to any objects which are using the DLL.
This method of working may seem rather stupid - a timer event is received by the DLL's
timer module, then posted to a form and intercepted from the form using the same DLL -
but it turns out this is vital to ensure the timer is stable during operation. There are
various examples of how to crash your system by not doing this available elsewhere on the
Internet (no names - you know who you are :)
When using the DLL provided with the download, you can get stable crash-free operation.
You can press stop in the VB IDE when the timer is running without any problems. Even
if you set the timer to do something at a silly rate which your code can't keep up with you
shouldn't have difficulty provided you use the Event interface, because the
PostMessage operation prevents things going wrong.
However, I should point out if you are running the DLL code in the group project,
no such niceties apply. No point giving disclaimers about when and how you will crash - you will!
It isn't a problem though, just restart VB. If you're interested in building this code into
your project, I strongly advise using the DLL to begin with and then only incorporate the code
when you come to make the EXE.
Using the HiRes Timer
The HiRes Timer offers a simple way of adding and removing timers, and there are two ways
of receiving timer events (the Third Way, as proposed by UK's increasingly bizarre
Prime Minister, will not be made available here).
- Responding to timers the first way involves setting up the class as WithEvents.
If you do this, the timer will call the Timer event every time it fires. This is the
simplest and easiest to debug method to use.
- The second way of receiving timer events is via an implemented interface. If the
calling object Implements the ITimer interface, you will have to implement
the ITimer_Timer(ByVal sKey As String) sub. If you do this, you should use the Connect
method to specify the object which receives the timer notifications. By doing this, you create an
early-bound interface between your code and the Timer, and you receive notifications with the
minimum of system overhead. This method is the fastest but can cause trouble as it can
occasionally overwhelm VB's IDE with event calls whilst you are debugging, something which
can't happen with the event interface. The best idea is to use the Events interface for development
and then switch to the implementation method for compiled executables.
The demonstration project supplied with the download demonstrates setting up two timer objects
and enabling/disabling them. It also compares the performance of the High-Resolution timer against
the standard VB Timer. On my Win95 system, the High-Resolution version manages to fire
40x more events than the standard VB version. (On NT4/2000/XP it manages about 10x more).
More!
If you aren't interested in the code for this sort of thing, then I recommend looking
at the High-Resolution timer available from the Common Controls Replacement Project (CCRP).
This offers extra interfaces such as a countdown timer and an About screen (Hmmm.)
Top code, but depends on your aim. If you want to create a MIDI or Audio sequencer, then
you need the code, and CCRP can't give you that for their own personal reasons.
An audio project has got to to be 1ms fast all the time and you don't want to know about
the hacks that some other coder went through to get their version to happen - you need to
make your own hacks and get it to run at the right speed. Hence the source code here.
One odd thing about the CCRP component is that it appears to be recommended as a general timer
- I'm not sure that the Multimedia one does this job. The VB Timer is a nice thing in all ways except
that it requires a form to site it on, but don't let that put you off.
VB only ever uses one timer resource no matter how many apps run on the system (because all VB apps by
definition are linked to the VB run-time library) so you are rarely in danger of running
out of timers.
|