3

It is important for a MIDI player to playback the notes as precisely as possible. I never quite succeeded in that, always blaming the timer (see a previous thread: How to prevent hints interrupting a timer). Recently I acquired ProDelphi and started measuring what exactly consumed that much time. The result was quite surprising, see the example code below.

procedure TClip_View.doMove (Sender: TObject; note, time, dure, max_time: Int32);
var x: Int32;
begin
{$IFDEF PROFILE}Profint.ProfStop; Try; Profint.ProfEnter(@self,1572 or $58B20000); {$ENDIF}
   Image.Picture.Bitmap.Canvas.Pen.Mode := pmNot;
   Image.Picture.Bitmap.Canvas.MoveTo (FPPos, 0);
   Image.Picture.Bitmap.Canvas.LineTo (FPPos, Image.Height);
   x := time * GPF.PpM div MIDI_Resolution;
   Image.Picture.Bitmap.Canvas.Pen.Mode := pmNot;
   Image.Picture.Bitmap.Canvas.MoveTo (x, 0);
   Image.Picture.Bitmap.Canvas.LineTo (x, Image.Height);
   FPPos := x;
//   Bevel.Left := time * GPF.PpM div MIDI_Resolution;
{$IFDEF PROFILE}finally; Profint.ProfExit(1572); end;{$ENDIF}
end; // doMove //

The measurements are (without debug code on an Intel i7-920, 2,7Ghz):

  1. 95 microseconds for the code as shown
  2. 5.609 milliseconds when all is commented out except for the now commented out statement (Bevel.Left :=)
  3. 0.056 microseconds when all code is replaced by x := time * GPF.PpM div MIDI_Resolution;

Just moving around a Bevel costs 60 times as much CPU as just drawing on a Canvas. That surprised me. The results of measurement 1 are very audible (there is more going on than just this), but 2 and 3 not. I need some form of feedback to the user as what the player now is processing, some sort of line over a piano roll is the accepted way. In my never ending quest of reducing CPU cycles in the timed-event loop I have some questions:

  • Why does moving around a bevel cost that much time?
  • Is there a way to reduce more CPU cycles than in drawing on a bitmap?
  • Is there a way to reduce the flicker when drawing?
Community
  • 1
  • 1
Arnold
  • 4,578
  • 6
  • 52
  • 91
  • Windows is not a realtime operating system, but you really shouldn't be tying the behaviour of realtime aspects (like your MIDI sequencing) to your UI painting. – Warren P Jan 28 '12 at 02:50
  • @WarrenP MIDI does not expect true "realtime". By itself, it has a latency of several ms (especially with DIN external peripheral - it is a serial link at 32000 bauds AFAIR). Today's Windows is reactive enough to handle this. – Arnaud Bouchez Jan 28 '12 at 10:06
  • But not if you insert random pauses in your code due to it being single threaded and inefficient. :-) – Warren P Jan 28 '12 at 12:55

3 Answers3

10

You won't be able to change the world, nor VCL nor Windows. I suspect you are asking to much to those...

IMHO you should better change a little bit your architecture:

  • Sound processing shall be in one (or more) separated thread(s), and should not be at all linked to the UI (e.g. do not send GDI messages from it);
  • UI refresh shall be made using a timer with a 500 ms resolution (half a second refresh sounds reactive enough), not every time there is a change.

That is, the sequencer won't refresh the UI, but the UI will ask periodically the sequencer what is its current status. This will be IMHO much smoother.

To answer your exact questions:

  • "Moving a bevel" is in fact sending several GDI messages, and the rendering will be made by the GDI stack (gdi32.dll) using a temporary bitmap;
  • Try to use a smaller bitmap, or try using a Direct X buffer mapping;
  • Try DoubleBuffered := true on your TForm.OnCreate event, or use a dedicated component (TPaintBox) with a global bitmap for the whole component content, with something like that for the message WM_ERASEBKGND.

Some code:

   procedure TMyPaintBox.WMEraseBkgnd(var Message: TWmEraseBkgnd);
   begin
     Message.Result := 1; // no erasing is necessary after this method call
   end;
Arnaud Bouchez
  • 42,305
  • 3
  • 71
  • 159
  • Double buffering slows things down of course and whilst it might help defeat flicker it will impact performance. Of course, decoupling audio from UI thread renders that moot. – David Heffernan Jan 27 '12 at 14:12
  • This is turning the architecture upside down! It is an interesting idea which I will try out because it decouples music production from visual reaction. Now I've linked everything together. Thanks! – Arnold Jan 27 '12 at 14:14
  • I decided to give you the accepted answer, not only because of the correct answer, but also because of your suggestion of decoupling. That greatly helped me to improve the program. I now have separate threads each with its own timer for audio and VCL and a message queue between them. That gives nice figures in the profiler. – Arnold Jan 27 '12 at 16:38
  • bullshit: *"Moving a bevel" is in fact sending several GDI messages*. If the bevel is already computed on an internal buffer, you don't have to move it...you just draw the buffer elsewhere. – az01 Jan 28 '12 at 01:36
  • 1
    @az01 Your comment is too much offensive. You should have better used the source, Luke! The `TBevel` sends GDI messages in `TControl.SetBounds` to invalidate the control, then a WM_PAINT message is sent back from the OS, and `TBevel.Paint` draws the bevel using GDI Canvas commands in the new place. It does not "just draw the buffer elsewhere". It draws the Bevel else were - there is "no bevel already computed on an internal buffer". – Arnaud Bouchez Jan 28 '12 at 10:18
3

I have the feeling that your bitmap buffering is wrong. When you move your clip it shouldn't have to be redrawn at all. you could try with This clip component structure:

TMidiClip = Class(TControl)
Private
 FBuffer: TBitmap;
 FGridPos: TPoint;
 FHasToRepaint: Boolean;
Public
  Procedure Paint; Override; // you only draw the bitmap on the control canvas
  Procedure Refresh; // you recompute the FBuffer.canvas
End;

When you change some properties such as "clip tick length" you set "FHasToRepaint" to true but not when changing "FGridPos" (position on the grid). So most of the time, in your Paint event, you only have a copy of your FBuffer.

Actually, this is very dependent on the design of your grid and its children (clips). I might be wrong but it seems that your design is not enough decomposed in Controls: the master grid should be a TControl, a clip should be a TControl, even the events on a clip should be some TControls...You can only define a strongly optimized bitmap-buffering system by this way (aka "double-buffering").

About the Timer: you should use a musical clock which processes per audio sample otherwise you can't have a good enough resolution. Such a clock can be implemented using the "Windows Audio driver" (mmsystem.pas) or an "Asio" driver (you have an interface in BASS, The Delphi Asio Vst project, for example).

az01
  • 1,988
  • 13
  • 27
  • 1
    I use the timer you advise. I don't use audio samples, just sending MIDI events. The averagee interval is 22 ms, but it should be able to process intervals of up to 10 ms. As to your answer about the bitmap I must confess my ignorance. I now directly draw upon the piano roll, which is contained in the Image.Picture.Bitmap. Do you suggest to keep a separate bitmap, draw on that and next SAtretchDraw it on the image bitmap? – Arnold Jan 27 '12 at 14:29
  • About the clock hust check this:[link](http://www.iceberg-softwares.com/release/delphi/Transport.zip). About the design of the grid I admit that my answer is a bit confused, but it's because this answer is mostly based on guesses at that a complex system of grid/grid-item would recquire a most advanced discussion. – az01 Jan 27 '12 at 21:19
2

By far the best way to tackle this is to stop the GUI message queue interfering with your MIDI player. Put the MIDI player on a background thread so that it can do its work without interruption from the main thread. Naturally this relies on you running on a machine with more than a single processor but it's not unreasonable to take that for granted nowadays.

Judging from your comments it looks like your audio processing is being blocked by the UI thread. Don't let that happen and your audio problems will disappear. If you are using something like TThread.Synchronize to fire VCL events from the audio thread then that will block on the UI thread. Use an asynchronous communication method instead.

Your proposed alternative of speeding up the VCL is not really viable. You can't modify the VCL and even if you could, the bottleneck could easily be the underlying Windows code.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • @DavidHeffeman, The MIDI player runs already in the background, the callback is processed by a timer thread. Some event handlers have to communicate with the VCL to tell the user the status of the playback. I did not propose to modify the VCL, just looking for ways to do things fast. Drawing on a bitmap appears to be faster than moving a compnent relative to another component. Just wondering if there are other techniques I didn' know. – Arnold Jan 27 '12 at 13:53
  • Perhaps I misunderstood. I thought your problem was that the audio playback was not accurate. Is the problem that your painting is too slow? – David Heffernan Jan 27 '12 at 13:56
  • You describe the problem correctly, maybe it's me who understands you wrong :-) The timer (from MMSystem) has a callback that runs in a thread. It checks on all kind of event handlers and some of these report back to the VCL to give feedback. It pays to reduce the CPU time in each element of this reporting chain. One of these elements is the `doMove` routine shown above. I found a way to do that but was just asking for more :-) And I was wondering why moving a Bevel relative to another component takes so much time. – Arnold Jan 27 '12 at 14:19
  • Ok then I think you need to communicate to the VCL asynchronously. Use TThread.Queue or some other asynchronous comma. Don't block in the audio thread. – David Heffernan Jan 27 '12 at 14:37
  • I'm saying just the same as Arnaud is saying. – David Heffernan Jan 27 '12 at 14:38
  • @MartinJames Indeed. That is presumably where the audio thread blocks on the UI thread. That's going to be the fundamental problem. – David Heffernan Jan 27 '12 at 15:08
  • I guess I just didn't explain it as well as Arnaud. Having a timer in the UI to drive UI updates is certainly a good solution. It switches from a push design to a pull design. It works for you I guess because you want regular, frequent updates of the UI. If you didn't update the UI on such a regular basis then push would be better, but an asynchronous push rather than your original synchronous push. – David Heffernan Jan 27 '12 at 16:45