이것은 1999년 하이텔 게임 제작 동호회에 올렸던 강좌 2회분 입니다.

#####################################################

 아래의 글은 3 가지의 타이머 (하드웨어적, API, 멀티미디어) 에 대한 저의 견해
와 개요를 적은 글입니다. 예전에 세미나 할 때 사용했던 글인데  그냥 썩혀 두기
도 그렇고 해서 강의란에 올립니다.

 그리고 아래부터는 존칭은 생략하겠습니다.

----------------------------------------------------------------------------

1. 하드웨어적 타이머 제어

 Windows95 라는 운영체제는 유저로 하여금 직접 하드웨어를 제어할 수 없는 구조
로 되어 있다. 예를 들어 메모리의 0xA000:0000 으로 매핑  되어졌던 그래픽 카드
에 대한 제어나 0x388 포트로 정의 되어졌던 FM 사운드 카드에 대한 제어를 할 수
없는 것이 바로 그것이다. 항상 운영체제의 API 를 거쳐야  제어가 가능하다는 점
은, 프로그램의 안정성이나 개발자들에 대해 내부 캡슐화로 인한  빠른 개발 기간
으로 그 장점이 대표되어 질  수도 있다. 하지만 게임이나 궁극적인  실시간 음악
(또는 음성) 연주 프로그램에서는 상당히 속도 면에서 불리한 것이다.

 예를 들자면 사운드 카드의 DMA 나 하드웨어 가속기능은 전혀 사용할 수 없게 되
는 것이기 때문에 각 카드의 장점을 살리지 못한 채 속도만 많이  잡아 먹는 골칫
덩이로 전락하게 된 것이다.
( 물론 기존 게임 업체들의  불만을 해소하고자, 사운드 카드의 DMA  와 하드웨어
가속을 지원하기 위해
  MS 에서는 DirectSound 라는 SDK 를 제공했다. )

어쨌든 윈도우에서는 운영체제에서 지원하는 함수 이외는 사용할  수 없으니 이전
의 도스 리얼 모드에서의 타이머 제어를 다루도록 한다.

1> 사운드 카드의 제어

 요즘에 나오는   모든 사운드  카드들이  기본적으로 호환   시키고 있는  AdLib
Compatible 은 그 원리가 FM(Frequency Modulation)을 기본으로 하고 있기 때문에
일정 주파수를 일정 딜레이 동안 나게 하면 그것이 바로 음원을 구성하게 되는 구
조이다. 참고로 MOD, S3M, IT 등의 모듈음악은 각  음원이 모듈로 소프트웨어적인
구성을 하고 있으며 MIDI 는 하드웨어적인 음원을 가지고 있다. 그 모듈음악은 음
원 정보가 모두 음악 파일 내부에 들어가기 때문에 MIDI 보다 훨씬  더 큰 사이즈
인 것이며 Adlib용 음악 파일인 ROL, IMS, MDI, SOD 등은 음원을 '뱅크'라는 개념
으로 가지고 있다.

 아래의 소스는 바로 AdLib 호환 사운드 카드에서 음을 발생 시키는 원리이다.

const
   ADLIB = $388;   // 애드립의 포트 번호

procedure AdLibOut(Address, Data : byte); assembler;
asm
  mov dx, ADLIB
  mov al, Address
  out dx, al

            mov  cx,   6
@@LOOP1:
  in  al, dx         // 사운드 카드가 명령을 해석하는
  loop @@LOOP1       // 동안 대기

  inc dx
  mov al, Data
  out dx, al
  dec dx

  mov cx, 35
@@LOOP2:
  in  al, dx         // 사운드 카드가 주파수 데이터를
  loop @@LOOP2       // 해석하는 동안 대기
end;

 위의 소스는 각 명령이 있는 Address  를 먼저 알린 후에 주파수  정보를 보내는
것이 전부이다. 하지만 이것으로 모든 음이 다 표현될 수  있다. 각각의 주파수를
각각 다른 딜레이를 통해 정의하기 때문에 피아노에서  드럼까지의 다양한 음원을
합성해 낼 수 있게 되는 것이다. ( 물론 이론상에서.. )

2> 타이머의 제어

 원래 도스에서 지원하는 클럭은 1초에 18.2 번 호출하게 되어  있는 시스템 시간
을 위한 클럭이다. 하지만 이것보다 더 빠른 클럭을 원한다면 Port 40h 부터 Port
43h 까지 할당되어 있는 8253 칩을 이용하는 방법이 있다. 아래의 소스는 클럭 주
파수를 다시 만드는 함수는 이며 이렇게 만든 클럭 주파수에 맞추어서 delay() 나
sleep() 같은 함수가 작동하게 된다.

 그럼..  정확한 타이머 딜레이를 주기 위한 방법도 알아 보자.

const
   FREQ_8253 = 1193280;
              // 이 값은 8253/8254 칩의 고유 쥬파수
procedure SetNewTimer(frequency : word);
begin
    port[$43] := $34;  // 8253 control word를 쓰겠다는 선언
       // $34 ==> 00110100
       //         | | |  +-- 2 진의 control type 을 선언..
       //         | | +----- 주파수 비율을 조정 하겠다...
       //         | +------- 래치의 상위, 하위 byte read 포맷으로..
       //         +--------- 타이머 0 번으로.. ( Clock이 쓰는 타이머임 )
    port[$40] := (FREQ_8253 div frequency) mod 256);
    port[$40] := (FREQ_8253 div frequency) div 256);
                       // 주파수를 나눈 값을 포트에 하위 8 bit 상위 8 bit 순
                       // 으로 써 넣음.
end;

 이렇게 클럭 최소 주파수를 조정하여서 인터럽트 0x08를 가로채면 자신의 타이머
핸들러가 만들어진다. 하지만 여기에는 단점이 생기는데 18.2 초에 한번씩 갱신되
게 되어 있는 PC 의 시계가 다른 주파수로 갱신되기 때문에 시계가 빨라지거나 느
려지는 현상이 발생한다. 이 때는 자신의 타이머 핸들러  안에다가 원래의 카운터
에 맞게 시계를 작동 시키는 루틴을 넣어야 하는 번거로운 절차가 따른다.

  <참고> 내부 타이머
     Voice 파일 출력 프로그램 등을 보면 타이머 2 번을 조정하기도 하는 것 같
     다. 타이머 2 번은 스피커 등에 사용되는 타이머인데 시계에 영향을 주지 않
     으니 장점이 있다.. 하지만 대부분의 실시간 음악 연주 루틴 등은 모두 타이
     머 0 번을 쓰고 있다.

3> 타이머를 통한 실시간 배경 음악 연주

 도스에서는 인터럽트 벡터를 가로채어서 비교적 윈도우 보다는 더욱 정밀한 타이
머 제어가 가능하다. 도스 인터럽트 0x08 번은 18.2 초마다 한번씩 시스템에 의해
서 불리어지는 인터럽트로 디폴트는 시간을 위한 카운터를  증가시킨 후에 함수를
리턴 시키는 것이다. 그렇기 때문에 우리가 실시간으로 음악을 연주 하려면 이 부
분의 인터럽트 벡터를 가로챔으로써 가능해지는 것이다.

 먼저 초기화 하는 부분부터 살펴 보자.

const
   TIMER_INTERRUPT = $08;  // 타이머 인터럽트에 대한 번호이다.
var
   OldInt08 : procedure;   // 이전의 인터럽트 벡터를 보관하기 위한 함수형 포
                           // 인터이다.

procedure ClockInterrupt; interrupt; assembler;
  // 이 함수는 인터럽트가 가능한 void 함수로 선언 되어 있다.

( 초기화 코드   )
   getIntVec(TIMER_INTERRUPT,@OldInt08);
   // OldInt08 이란 변수에 현재 0x08 번 인터럽트 벡터를 저장한다.
   setIntVec(TIMER_INTERRUPT,@ClockInterrupt);
   // 0x08 번 인터럽트 벡터가 ClockInterrupt 를 가리키게 한다.
   setNewTimer(298295 div basic_tempo);
   // 현재의 클럭을 조절한다.

 물론 종료 코드는 다음과 같이 초기화의 반대로 하면 된다.

( 종료 코드 )
   setNewTimer(0);
   // 클럭 호출 빈도를 디폴트로 바꾼다.
   setIntVec(TIMER_INTERRUPT,@OldInt08);
   // 0x08 번 인터럽트를 예전의 것으로 복귀 시킨다.

 이렇게 하면 유저가 지정한 클럭을 기준으로 0x08 인터럽트가 호출되게 된다.
 그럼 먼저 실시간 배경 음악을 위한 핵심 함수인 ClockInterrupt 를 살펴 보기로
하자.

 

var
   TimeOutFunction : function : word;
     // 음악을 실제적으로 연주하는 부분으로 미리 함수의 포인터로 대입되어 져
있다.
procedure ClockInterrupt; interrupt; assembler;
     // 이 함수는 최대한 빨리 수행 되어야 하므로 어셈블러로 제작 되어졌다.
asm
 MOV     AX, clock_division_low   // 유저가 변경한 클럭은 이전의   
 ADD     clock_mode, AX           // 클럭과는 주파수가 다르기 때문에
 MOV     AX, clock_division_high  // 이전의 주파수와 동기화 시켜서
 ADC     AX, 0                    // OldInt08 를 실행 해야 한다.
 JNZ     @@ClockInterrupt8        // 그렇지 않으면 시계가 빨리 가 버린다.
 MOV     AL, 00100000b             
 OUT     20h, AL
 JMP     @@ClockInterrupt7

@@ClockInterrupt8:
 PUSHF                            // 플래그를 스택에 저장
 CALL   OldInt08                  // 시스템 시계를 가게 하기 위해서
                                  // 1 초에 18.2 번 호출
@@ClockInterrupt7:
 DEC     sound_delay              // 아직도 음이 딜레이 중이면
 JNZ     @@ClockInterruptEnd      // 함수를 종료

@@GoUser:

 STI                              // 인터럽트를 가능하게 한다
 CALL    TimeOutFunction;         // 음악 데이터를 읽고 포트로 츨력
 CLI                              // 모든 인터럽트를 불가능하게 한다

 MOV     BX, sound_delay          // if (-sound_delay < ax) break;
 NEG     BX
 CMP     BX, AX
 JB      @@ClockDelayOk

 MOV     sound_delay, 0
 JMP     @@GoUser

@@ClockDelayOk:
 ADD     sound_delay, AX

 STI

@@ClockInterruptEnd:
end;


2. API 타이머

1> API 타이머의 개요

 타이머는 어떤 프로그램 안에서 주기적으로 행하여 지는 작업이  필요할 때 유용
하게 사용되는데, 32 비트 운영체제인 Window 95에서는  2,500개 이상의 타이머를
지원한다.
 타이머의 선언은 SetTimer()에 의해서  선언이 되어지며 그때 얻은  타이머 ID를
통해서 모든 제어가 이루어진다.  타이머가 선언되면 그 타이머가  해제될 때까지
주기적으로 이벤트 또는 콜백 함수를 호출하게 되며, 프로그램이 끝나면 자동적으
로 타이머는 종료된다.

2> API 타이머의 선언 방법

 API 타이머는 다음의 원형으로 정의된 SetTimer() 함수로 설정을 한다.

   UINT  SetTimer(HWND hWnd, UINT uTimerID, UINT uInterval,
                  TIMERPROC fnTimerProc)

      hWnd        : 타이머를 가지고 있는 윈도우의 핸들, NULL 일 때는 윈도우
                    연관 없음
      uTimerID    : 정의 하고자 하는 타이머 번호, 0이면 시스템이 알아서 결정
      uInterval   : mili second 기준의 이벤트 발생 간격
      fnTimerProc : NULL 이면 WM_TIMER 메세지 발생, 만약 함수라면 그 함수를
                    콜백 함수로.
      반환값  -  설정되어진 타이머 ID

 여기서 마지막의 fnTimerProc에 의해서 메세지 콜백의 형태가 2가지로 정해진다.

  1) 메세지 발생
    : hWnd 로 넘겨진 윈도우 핸들을 가진 인스턴스로 WM_TIMER 라는 메세지가 넘
     어가게 된다. 프로그래머는 그 메세지를 case로 분류해서 루틴을 짜면 된다.

  2) 콜백 함수
    : void CALLBACK TimerProc(HWND hWnd, UNIT uMsg, UINT idEvent, DWORD
     dwTime)으로 정의되어진 콜백 함수를 이벤트가 발생하면 호출한다.
    

3> API 타이머의 단점

 윈도우는 많은 타이머들의 호출에 의한 시스템 마비 현상을  없애기 위해 주어진
시간에 단 한 개의  타이머 메세지(WM_TIMER)만을 받고 나머지는  무시해 버린다.
또한 WM_PAINT를 제외한 모든 윈도우의 메세지가 WM_TIMER  보다 우선하기 때문에
API 타이머는 원래 정해진 시간과는 다소의 오차가 발생하게 된다.
대부분의 응용 프로그램에서는 이러한 타이머의 지연에 대해서는  간과해 버릴 수
있으나 멀티미디어나 게임의 동기화에서는 API 타이머로는 만족할 수  없는 게 사
실이다.

3. 멀티미디어 타이머

1> 멀티미디어 타이머의 개요

 전술한 API 표준 타이머는 메세지 방식의 우선 순위 때문에  정교한 제어를 위해
서는 별로 유용하지 못하다. 게다가 최대 빈도수가 초당 18 타임이기 때문에 그보
다 더 정확한 1/1000 초를 제어할 수 있는 타이머가 요구되어 졌는데 그것이 바로
이 멀티미디어 타이머이다. 이것은 MIDI 시퀀스 장치 또는  다른 전문 하드웨어에
대한 MIDI 이벤트 제어처럼 애플리케이션의 정확한 타이밍을  요구할 때 사용되어
진다.

 이 멀티미디어  타이머가 API  타이머와 다른   점이라면 Timer Event  Callback
Notification 메커니즘이라는 것이다. ( API 타이머는 애플리케이션  메세지 큐에
WM_TIMER 삽입 )

2> 함수 요약

    timeGetDevCaps    타이머가 지원하는 최소 최대 해상도를 알아낸다.
    timeBeginPeriod   최소의 타이머 해상도를 결정한다.
    timeEndPeriod     timeBeginPeriod 에 의해 만들어진 타이머 해상도를 제거
                      한다.
    timeSetEvent      타이머 이벤트를 시작하고 간격에 따라 콜백 함수를 호출
                      한다.
    timeKillEvent     타이머 이벤트를 끝낸다.

 여기서 timeBeginPeriod 에 의해 넘겨지는 밀리 초 단위의 최소 타이머 해상도는
그 해상도에 대한 ID 처럼 생각할 수 있으며 timeEndPeriod 에 의해 제거 될 때도
그 최소 해상도를 넘겨 주어야 제거 가능하다. timeSetEvent  로 타이머 이벤트를
시작하기 전에   항상 최소  타이머  해상도가 정해져야   하며 타이머  이벤트가
timeKillEvent 로 제가 된 후에는 항상 제거 되어져야 한다.

 또, 정확한  밀리  초 단위의   시간을 얻고자  할 때는   timeGetSystemTime 과
timeGetTime 을 사용하면 되는데 이  두개의 함수는 API 타이머에서  같은 기능을
하는 GetTickCount() 나 GetCurrentTime() 와는 달리 실질적인 밀리 초 정보를 제
공한다.

3> 멀티미디어 타이머의 단점

 이것은 상당히 정확한 이벤트를 제공하는 반면에 그 만큼  시스템에 많은 부담을
주게 되므로 여러 개의 타이머를  동시에 돌리기에는 많은 시스템의  부하가 따른
다.

 또 하나의 문제는 콜백 함수의  제한성에 있다. API 타이머의 콜백  함수는 일반
메세지와 같이 처리 되기 때문에 아무런 제약이  없었으나 멀티미디어 타이머에서
는 콜백 함수 내에서 사용할 수 있는 함수가 정해져 있고, 정해진 함수 이외의 사
용자 함수나 API 등을 호출하면 예측할 수 없는 결과를 초래하게 된다. 정해진 함
수란,  PostMessage(),  timeGetSystemTime(),  timeGetTime(),  timeSetEvent(),
timeKillEvent(),         midiOutShortMessage(),        midiOutLongMessage(),
OutputDebugString() 이 전부이다. PostMessage()와  OutputDebugString() 을  제
외하고는 모두 멀티미디어 함수들 뿐이다. 즉, 멀티미디어 용도 외에 타이머를 사
용하려면 모두 PostMessage()를 사용하여 다시 메세지를 발생해야 하는 것이다. (
API 타이머 보다야 빠르겠지만 대단한 클럭 낭비다. )

 

#####################################################

이번 장에서는 'API 타이머'와 '멀티미디어 타이머'의 용법과 함께  그 성능에 대
해서 논하고자 한다. 이 강좌의 소스는 모두 델파이로 제작되었으므로 C 사용자들
은 아쉬운 대로 눈으로 읽고 이해하시길 바란다.

unit Unit1;

interface

uses
   Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
   ExtCtrls, StdCtrls;

type
   TForm1 = class(TForm)
      Edit1       : TEdit;        // 타이머 콜백된 회수 출력
      Edit2       : TEdit;        // 초당 콜백 회수
      Button1     : TButton;      // 타이머 종료 버튼
      StaticText1 : TStaticText;  // 라벨 1
      StaticText2 : TStaticText;  // 라벨 2
      Panel1      : TPanel;       // 현재 설정된 콜백 주기 출력
      procedure FormShow(Sender: TObject);
      procedure FormDestroy(Sender: TObject);
      procedure Button1Click(Sender: TObject);

   private
      FTimerID: word;
      Fms     : word;
      FCouter : integer;
      FTime   : longint;
   end;

var
   Form1 : TForm1;

implementation

{$R *.DFM}

// API 타이머 콜백 함수
procedure TimerProc(handle : hWnd; uMessage,
                    idEvent : word; dwTime : DWORD);
var
   f : real;
   s : string;
begin
   inc(Form1.FCouter);
   str(Form1.FCouter,s);
   Form1.Edit1.Text := s;

   f := (Form1.FCouter * 1000) / (GetCurrentTime - Form1.FTime);
   str(f : 6 : 3,s);
   Form1.Edit2.Text := s;  // 초당 콜백 회수 출력

   if Form1.FCouter = 0 then begin
      Form1.FTime := GetCurrentTime;  // 콜백 카운트의 초기화
   end;
end;

procedure TForm1.FormShow(Sender: TObject);
var
   s        : string;
begin
   Fms := 100;  // 타이머 주기를 100ms 마다 한 번씩 호출되게 한다.
   FTimerID := SetTimer(handle,0,Fms,@TimerProc);
                // 타이머를 설정한다.
   if FTimerID = 0 then begin
      raise Exception.Create('SetTimer() Error !!');
      Close;
      Exit;     // 에러 처리 루틴
   end;

   str(Fms,s);
   Panel1.Caption := '타이머 설정 :' + s + 'ms';

   FCouter := -1;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
   if FTimerID <> 0 then begin
      KillTimer(handle,FTimerID);
                // 타이머 해제
   end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
   Close;
end;

end.

아래는 폼에 대한 Text 파일이다. Form 에서 오른쪽 버튼을 누르면 생기는 메뉴에
서 View as Text 를 선택한 후에 아래의 소스로 대체하고 다시  오른쪽 버튼을 눌
러서 View as Form 을 선택하면 원래의 Form 이 형성되게 된다.

object Form1: TForm1
  Left = 276
  Top = 145
  Width = 124
  Height = 214
  Caption = 'Form1'
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'MS Sans Serif'
  Font.Style = []
  OnDestroy = FormDestroy
  OnShow = FormShow
  PixelsPerInch = 96
  TextHeight = 13
  object Edit1: TEdit
    Left = 0
    Top = 56
    Width = 113
    Height = 21
    ImeName = '한국어(한글)'
    TabOrder = 0
  end
  object Edit2: TEdit
    Left = 0
    Top = 104
    Width = 113
    Height = 21
    ImeName = '한국어(한글)'
    TabOrder = 1
  end
  object Button1: TButton

   Left = 24
    Top = 144
    Width = 75
    Height = 25
    Caption = 'Stop !!'
    TabOrder = 2
    OnClick = Button1Click
  end
  object StaticText1: TStaticText
    Left = 0
    Top = 40
    Width = 116
    Height = 17
    Alignment = taCenter
    Caption = '타이머 콜백 호출'
    Font.Charset = HANGEUL_CHARSET
    Font.Color = clWindowText
    Font.Height = -13
    Font.Name = '돋움체'
    Font.Style = []
    ParentFont = False
    TabOrder = 3
  end
  object StaticText2: TStaticText
 Left = 8
    Top = 88
    Width = 102
    Height = 17
    Caption = '초당 콜백 회수'
    Font.Charset = HANGEUL_CHARSET
    Font.Color

= clWindowText
    Font.Height = -13
    Font.Name = '돋움체'
    Font.Style = []
    ParentFont = False
    TabOrder = 4
  end
  object Panel1: TPanel
    Left = 0
    Top = 0
    Width = 116
    Height = 25
    Align = alTop
    BevelInner = bvLowered
    Font.Charset = HANGEUL_CHARSET
    Font.Color = clWindowText
    Font.Height = -11
    Font.Name = '돋움체'
    Font.Style = []
    ParentFont = False
    TabOrder = 5
  end
end

위의 소스가 이해 되었다면 직접 실행 시켜 보자. 우리는 처음에  타이머 콜백 주
기를 100ms 로 설정했고, 예상되어지는 초당 콜백 회수는 10번이다.
하지만 실제로 1 분 정도 실행  시켜보면 초당 9.036 번의 콜백이  이루어 진다는
것을 알 수 있다 ( 펜티엄 2 266Mz 에서 실험 ). 이것은 API  타이머가 우리가 원
하는 것 이하의 성능을 낸다는 것을 말해 주고 있는데, 이  결과는 다음에 확인해
볼 멀티미디어 타이머의 정확도에 비해 많은 차이를 보인다.

----------------------------------------------------------------------------

그럼, 다음은 멀티미디어 타이머의 성능을 평가하는 예제이다.
폼에 대한 것은 위의 API 타이머의 폼과같이 쓰면 된다.

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  ExtCtrls, StdCtrls, mmSystem;

const
   USR_TIMER_ELAPSED = WM_USER+100;
type
   TForm1 = class(TForm)
      Edit1       : TEdit;        // 타이머 콜백된 회수 출력
      Edit2       : TEdit;        // 초당 콜백 회수
      Button1     : TButton;      // 타이머 종료 버튼
      StaticText1 : TStaticText;  // 라벨 1
      StaticText2 : TStaticText;  // 라벨 2
      Panel1      : TPanel;       // 현재 설정된 콜백 주기 출력
      procedure FormShow(Sender: TObject);
      procedure FormDestroy(Sender: TObject);
      procedure Button1Click(Sender: TObject);

   private
      FTimerID: UINT;
      Fms     : UINT;
      FCouter : integer;
      FTime   : longint;

      procedure EditCallback(var M : TMessage); message USR_TIMER_ELAPSED;
         // 사용자 메시지를 받기 위한 메시지 처리 루틴
   end;

var
   Form1 : TForm1;

implementation

{$R *.DFM}

procedure TForm1.EditCallback(var M : TMessage);
var
   f : real;
   s : string;
begin
   inc(Form1.FCouter);
   str(Form1.FCouter,s);
   Form1.Edit1.Text := s;

   f := (Form1.FCouter * 1000) / (GetCurrentTime - Form1.FTime);
   str(f : 6 : 3,s);
   Form1.Edit2.Text := s;  // 초당 콜백 회수 출력

   if Form1.FCouter = 0 then begin
      Form1.FTime := GetCurrentTime;  // 콜백 카운트의 초기화
   end;
end;

procedure TimerProc(uTimer, uMessage : UNIT; dwUser, dw1, dw2 :  DWORD); stdcall;
begin
   PostMessage(dwUser,USR_TIMER_ELAPSED,0,0);
      // 멀티미디어 타이머 콜백 함수에는 대부분의 API가 사용 불가하다.
      // 그러므로 다시 메시지를 사용자 메시지 루틴으로 돌려야 한다.
end;

procedure TForm1.FormShow(Sender: TObject);
var
   s        : string;
   TimeCaps : TTimeCaps;
begin
   Fms := 100;  // 타이머 주기를 100ms 마다 한 번씩 호출되게 한다.
   if timeGetDevCaps(@TimeCaps,sizeof(TimeCaps)) <> TIMERR_NOERROR then
      raise Exception.Create('<timeGetDevCaps> Error !!');
      // 타이머 디바이스에 대한 정보를 얻어 온다.

   if Fms < TimeCaps.wPeriodMin then
      Fms := TimeCaps.wPeriodMin; // 실제 가능한 호출 주기보다 작으면
   if Fms > TimeCaps.wPeriodMax then
      Fms := TimeCaps.wPeriodMax; // 실제 가능한 호출 주기보다 크면

   timeBeginPeriod(Fms); // 타이머 주기를 지정한다.
   FTimerID := timeSetEvent(Fms,Fms,@TimerProc,integer(handle),
                            TIME_PERIODIC);
      // 타이머를 설정한다. 콜백 함수를 지정하고, TIME_PERIODIC를 사용하여
      // 주기적으로 계속 호출되게 만든다.
     
   if FTimerID = 0 then begin
       timeEndPeriod(Fms);
       raise Exception.Create('<timeSetEvent> Error !!');
       Close;
       Exit; // 에러 처리 루틴
   end;

   str(Fms,s);
   Panel1.Caption := '타이머 설정 :' + s + 'ms';

   FCouter := -1;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
   if FTimerID <> 0 then begin
      timeKillEvent(FTimerID); // 타이머를 해제한다.
      timeEndPeriod(Fms);      // 타이머 주기를 해제한다.
   end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
   Close;
end;

end.

위의 소스를 실행해 보면 항상 0.005 초 정도의 오차만을 가지고  초당 10 회라는
콜백 회수를 계속 유지한다. ( 10.000 초를 기준으로 진동한다고 보는 게 나을 것
같다. )
이렇듯 멀티미디어 타이머는 시스템 자원을 많이 먹는 대신 정확한 타이머 제어가
가능하므로 대부분의 정밀 작업이나 실시간 배경음악 출력에 쓰이게 된다.

직접 눈으로 확인했다면 아마도  API 타이머와 멀티미디어 타이머에  대한 적절한
용법이 떠올랐으리라고 본다. 그리고  나머지 활용에 대한 것은  전적으로 당신의
몫인 것이다.........

                                              ------------- SMgal ( 안영기 )