src/midiinput.pas5.6 KB · ObjectPascal
Raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
unit MidiInput;

{
  Cross-platform MIDI input for hUGETracker using PortMidi.

  This unit owns a single global TMidiInput instance, hosts a TTimer that
  polls the currently open MIDI input stream on the main thread (so callers
  don't have to worry about synchronizing with the LCL), and raises
  OnNoteOn / OnNoteOff events with hUGETracker-scaled note numbers.

  Notes are translated from MIDI's 0..127 range to hUGETracker's 0..71
  (C_3..B_8). MIDI C3 (48) maps to hUGETracker C_3 (0); anything outside
  the tracker's range is silently dropped so we never feed an out-of-bounds
  value into the preview code.
}

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, ExtCtrls, portmidi;

const
  HT_MIDI_NOTE_OFFSET = 48; // MIDI C3 -> hUGETracker C_3 (note index 0)

type
  TMidiNoteEvent = procedure(Sender: TObject; Note, Velocity: Integer) of object;

  TMidiInput = class
  private
    FStream: PPortMidiStream;
    FTimer: TTimer;
    FDeviceName: String;
    FOnNoteOn: TMidiNoteEvent;
    FOnNoteOff: TMidiNoteEvent;
    FInitialized: Boolean;
    FAvailable: Boolean;
    procedure HandleTimer(Sender: TObject);
    procedure DispatchMessage(msg: PmMessage);
  public
    constructor Create;
    destructor Destroy; override;

    function Available: Boolean;
    procedure EnumerateInputDevices(Names: TStrings);
    function FindInputDeviceID(const Name: String): PmDeviceID;
    function OpenDevice(const Name: String): Boolean;
    procedure CloseDevice;
    function IsOpen: Boolean;
    property DeviceName: String read FDeviceName;

    property OnNoteOn: TMidiNoteEvent read FOnNoteOn write FOnNoteOn;
    property OnNoteOff: TMidiNoteEvent read FOnNoteOff write FOnNoteOff;
  end;

function Midi: TMidiInput;

// Translate raw MIDI (0..127) to hUGETracker note index. Returns -1 if
// the note is outside the tracker's range.
function MidiNoteToTrackerNote(MidiNote: Integer): Integer;

implementation

var
  GMidi: TMidiInput = nil;

function Midi: TMidiInput;
begin
  if GMidi = nil then
    GMidi := TMidiInput.Create;
  Result := GMidi;
end;

function MidiNoteToTrackerNote(MidiNote: Integer): Integer;
begin
  Result := MidiNote - HT_MIDI_NOTE_OFFSET;
  if (Result < 0) or (Result > 71) then
    Result := -1;
end;

{ TMidiInput }

constructor TMidiInput.Create;
begin
  inherited Create;
  FStream := nil;
  FAvailable := LoadPortMidi;
  if FAvailable then begin
    if Pm_Initialize() = pmNoError then
      FInitialized := True
    else
      FAvailable := False;
  end;

  FTimer := TTimer.Create(nil);
  FTimer.Interval := 5;
  FTimer.Enabled := False;
  FTimer.OnTimer := @HandleTimer;
end;

destructor TMidiInput.Destroy;
begin
  CloseDevice;
  FTimer.Free;
  if FInitialized then
    Pm_Terminate();
  inherited Destroy;
end;

function TMidiInput.Available: Boolean;
begin
  Result := FAvailable;
end;

procedure TMidiInput.EnumerateInputDevices(Names: TStrings);
var
  I, N: Integer;
  Info: PPmDeviceInfo;
begin
  Names.Clear;
  if not FAvailable then Exit;

  N := Pm_CountDevices();
  for I := 0 to N - 1 do begin
    Info := Pm_GetDeviceInfo(I);
    if (Info <> nil) and (Info^.input <> 0) then
      Names.Add(String(Info^.name));
  end;
end;

function TMidiInput.FindInputDeviceID(const Name: String): PmDeviceID;
var
  I, N: Integer;
  Info: PPmDeviceInfo;
begin
  Result := pmNoDevice;
  if (not FAvailable) or (Name = '') then Exit;

  N := Pm_CountDevices();
  for I := 0 to N - 1 do begin
    Info := Pm_GetDeviceInfo(I);
    if (Info <> nil) and (Info^.input <> 0) and (String(Info^.name) = Name) then
      Exit(I);
  end;
end;

function TMidiInput.OpenDevice(const Name: String): Boolean;
var
  DevID: PmDeviceID;
begin
  Result := False;
  if not FAvailable then Exit;

  CloseDevice;

  DevID := FindInputDeviceID(Name);
  if DevID = pmNoDevice then Exit;

  if Pm_OpenInput(FStream, DevID, nil, 256, nil, nil) <> pmNoError then begin
    FStream := nil;
    Exit;
  end;

  FDeviceName := Name;
  FTimer.Enabled := True;
  Result := True;
end;

procedure TMidiInput.CloseDevice;
begin
  FTimer.Enabled := False;
  if FStream <> nil then begin
    Pm_Close(FStream);
    FStream := nil;
  end;
  FDeviceName := '';
end;

function TMidiInput.IsOpen: Boolean;
begin
  Result := FStream <> nil;
end;

procedure TMidiInput.DispatchMessage(msg: PmMessage);
var
  Status, Data1, Data2: Byte;
  Kind, TrackerNote: Integer;
begin
  Status := Pm_MessageStatus(msg);
  Data1 := Pm_MessageData1(msg);
  Data2 := Pm_MessageData2(msg);

  Kind := Status and $F0;
  TrackerNote := MidiNoteToTrackerNote(Data1);
  if TrackerNote < 0 then Exit;

  case Kind of
    $90: // Note On
      begin
        // MIDI convention: Note On with velocity 0 is a Note Off.
        if Data2 = 0 then begin
          if Assigned(FOnNoteOff) then FOnNoteOff(Self, TrackerNote, 0);
        end
        else begin
          if Assigned(FOnNoteOn) then FOnNoteOn(Self, TrackerNote, Data2);
        end;
      end;
    $80: // Note Off
      if Assigned(FOnNoteOff) then FOnNoteOff(Self, TrackerNote, Data2);
  end;
end;

procedure TMidiInput.HandleTimer(Sender: TObject);
const
  BUF_SIZE = 32;
var
  Buf: array[0..BUF_SIZE-1] of TPmEvent;
  Count, I: Integer;
  PollResult: PmError;
begin
  if FStream = nil then Exit;

  PollResult := Pm_Poll(FStream);
  if PollResult < 0 then begin
    // Device likely removed - close cleanly so we stop hammering it.
    CloseDevice;
    Exit;
  end;

  while Pm_Poll(FStream) = pmGotData do begin
    Count := Pm_Read(FStream, @Buf[0], BUF_SIZE);
    if Count <= 0 then Break;
    for I := 0 to Count - 1 do
      DispatchMessage(Buf[I].message_);
  end;
end;

finalization
  if GMidi <> nil then begin
    GMidi.Free;
    GMidi := nil;
  end;

end.