delphimiditempo

Can someone help me with tempo MIDI command on delphi 7?


I know how to generate sounds and percussion on midi, but I am needing to increase the tempo up and down, how I am not knowing how to do that, I am using multimedia timer, but the problem is that I am having the thread error when using two or more voices at the same time.

Here is for create the loop, it goes from 1 to 4.

procedure TimeCallBack(TimerID, Msg: Uint; dwUser, dw1, dw2: DWORD); pascal;
var
  nome: Integer;
begin
  nome := FrmMetrolima.ListBox1.ItemIndex;
  FrmMetrolima.panel518.Caption := FrmMetrolima.ListBox1.Items[nome];

  if (FrmMetrolima.panel518.Caption = 'Binário-2 (pág. 13)') then

  // Binário-2
  begin
    FrmMetrolima.panel3.Caption := '8';
    inc(tempo, 1);
    FrmMetrolima.Panel2.Caption := IntToStr(tempo);
    if FrmMetrolima.Panel2.Caption = '5' then
    begin
      FrmMetrolima.Panel2.Caption := '1';
      tempo := 1;
    end;
    // metrônomo
    if (FrmMetrolima.Panel2.Caption = '1') and
      (FrmMetrolima.Panel28.Caption = '1') and
      (FrmMetrolima.Label7.Caption = '1') then
    begin
      noteOn(9, 75, 127);
    end
    else if (FrmMetrolima.Panel2.Caption = '2') and
      (FrmMetrolima.Panel29.Caption = '2') and
      (FrmMetrolima.Label7.Caption = '1') then
    begin
      noteOn(9, 62, 127);
    end
    else if (FrmMetrolima.Panel2.Caption = '3') and
      (FrmMetrolima.Panel30.Caption = '3') and
      (FrmMetrolima.Label7.Caption = '1') then
    begin
      noteOn(9, 63, 127);
    end
    else if (FrmMetrolima.Panel2.Caption = '4') and
      (FrmMetrolima.Panel31.Caption = '4') and
      (FrmMetrolima.Label7.Caption = '1') then
    begin
      noteOn(9, 62, 127);
    end;

  end;
end;

Usage:

if (ListBox1.ItemIndex = 0) then
// Subdivisão Binária
begin
  noteOn(9, 75, 127);
  mmResult := TimeSetEvent(60000 div StrToInt(FlatEdit2.Text) div 2, 0,
    @TimeCallBack, 0, TIME_PERIODIC);

end;

Solution

  • At the time of writing this answer you have presented the TimeCallBack procedure and how you call TimeSetEvent. The following issues you need to correct.

    1.TimeCallBack is executed in the thread of the timer but you are accessing UI elements of the main thread. The VCL is not threadsafe and you can therefore not directly access any UI elements in the timer call back. If you really must access the UI in TimeCallBack I suggest using synchronize, which ensures the code is executed in the context of the main thread

    However, there's a better way which leads me to the second issue

    2.You are mixing program logic and UI. This is a broad topic but shortly: Create a data structure for the data that the program logic requires. In this case it could be a one dimensional, four element array of records. The record could have fields to hold one note, that is the parameters you pass to noteOn().

    type
      TNote = record
        chan: byte;
        sound: byte;
        volume: byte;
      end;
      TNoteSequence = array of TNote;
    

    In addition you need an index variable to keep track of which element to play at the TimeCallBack.

    Thus, if the array is called NoteSeq and the index variable is called indx, the TimeCallBack procedure would become

    procedure TimeCallBack(TimerID, Msg: UINT; dwUser, dw1, dw2: DWORD); stdcall;
    begin
      with NoteSeq[indx] do
        NoteOn(Chan, Sound, Volume);
      indx := (indx +1) mod 4;
    end;
    

    As you see, there is no need to touch the UI elements and thus you don't need to use synchronize. I have no clue what the purpose of the various Panelxx.caption are, but basically you should only need to check whether the metronome should run or not, and change the tempo(I'll return to this shortly). To run the metronome you call

    procedure TForm5.MidiPlayBtnClick(Sender: TObject);
    begin
      mmResult := TimeSetEvent(60000 div Bpm, 0, @TimeCallBack, 0, TIME_PERIODIC);
    end;
    

    (The Bpm is a variable holding Beats Per Minute, see below)

    3.The calling convention of the TimeCallBack procedure is declared as pascal which is not correct. Windows APIs use stdcall or safecall. Change to stdcall. For more information on calling conventions see documentation Scroll down to Calling Conventions.

    Changing tempo

    Finally to change the tempo in your setup, where playing the notes is driven by the timer, you obviously need to change the timer interval. Please note that according the documentation, the timeSetEvent is deprecated and you should use CreateTimerQueueTimer instead. However, since I have no experience of that, I will continue building on timeSetEvent instead. To achieve 120 Beats Per Minute (BPM) you would calculate 60000 div Bpm, which is 500 ms. Add two variables to your data, Bpm: integer; and BpmChanged: boolean; In the UI you could use a TUpDown to change the Bpm and whenever the Bpm is changed you set BpmChanged true. To have a smooth change in tempo you change the timer in the TimeCallBack. The interval of a running timer can not be changed, therefore you need to kill the running timer and start a new one.

    procedure TimeCallBack(TimerID, Msg: UINT; dwUser, dw1, dw2: DWORD); stdcall;
    begin
      with NoteSeq[indx] do
        NoteOn(Chan, Sound, Volume);
      indx := (indx +1) mod 4;
      if BpmChanged then
      begin
        timeKillEvent(mmResult);
        mmResult := TimeSetEvent(60000 div Bpm, 0, @TimeCallBack, 0, TIME_PERIODIC);
        BpmChanged := False;
      end;
    end;