Commit fad0b1a

Nick Faro committed on
midi input
commit fad0b1a694c4d1879b52bbc05dd673381abf35a7 parent a63622a
11 changed files +817−34
Added.github/scripts/build-portmidi.sh +50−0
@@ -0,0 +1,50 @@
1 #!/usr/bin/env bash
2 # Clones and builds PortMidi as a shared library. The resulting install tree
3 # is placed in $GITHUB_WORKSPACE/portmidi-install so the packaging steps for
4 # each platform can copy the right files out of it.
5 #
6 # PortMidi is built against:
7 # - ALSA on Linux (requires libasound2-dev)
8 # - CoreMIDI on macOS
9 # - WinMM on Windows via MSYS2/MinGW
10 set -eux
11
12 PORTMIDI_REF="v2.0.5"
13
14 if [ "$(uname -s)" = "Darwin" ]; then
15 JOBS=$(sysctl -n hw.ncpu)
16 else
17 JOBS=$(nproc 2>/dev/null || echo 2)
18 fi
19
20 # $GITHUB_WORKSPACE isn't set on all shells (e.g. MSYS2 with path-type strict),
21 # but the working directory at job start *is* the workspace. Resolve it
22 # explicitly so install paths are predictable on every runner.
23 WORKSPACE="$(pwd)"
24 INSTALL_PREFIX="${WORKSPACE}/portmidi-install"
25
26 if [ -d portmidi-src ]; then
27 rm -rf portmidi-src
28 fi
29
30 git clone --depth 1 --branch "${PORTMIDI_REF}" https://github.com/PortMidi/portmidi.git portmidi-src
31 cd portmidi-src
32
33 # PortMidi defaults BUILD_SHARED_LIBS=OFF; flip it so we get a .so/.dylib/.dll
34 # we can ship next to the hUGETracker binary.
35 CMAKE_EXTRA=()
36 if [ "$(uname -s | head -c 5)" = "MINGW" ] || [ "$(uname -s | head -c 5)" = "MSYS_" ] || [ "${OS:-}" = "Windows_NT" ]; then
37 CMAKE_EXTRA+=(-G "MSYS Makefiles")
38 fi
39
40 cmake -S . -B build \
41 -DCMAKE_BUILD_TYPE=Release \
42 -DBUILD_SHARED_LIBS=ON \
43 -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" \
44 "${CMAKE_EXTRA[@]}"
45
46 cmake --build build --config Release -j${JOBS}
47 cmake --install build --config Release
48
49 ls -l "${INSTALL_PREFIX}"
50 find "${INSTALL_PREFIX}" -maxdepth 3 -type f -o -type l | sort
Modified.github/workflows/build.yml +23−1
@@ -33,7 +33,7 @@ jobs:
33 if: matrix.name == 'Linux' 33 if: matrix.name == 'Linux'
34 run: | 34 run: |
35 sudo apt -qq update 35 sudo apt -qq update
36 sudo apt install -yq libsdl2-dev bison 36 sudo apt install -yq libsdl2-dev bison libasound2-dev
37 curl -Lo rgbds.tar.gz 'https://github.com/gbdev/rgbds/releases/download/v0.8.0/rgbds-0.8.0.tar.gz' 37 curl -Lo rgbds.tar.gz 'https://github.com/gbdev/rgbds/releases/download/v0.8.0/rgbds-0.8.0.tar.gz'
38 tar xvf rgbds.tar.gz rgbds/{include,src,Makefile} 38 tar xvf rgbds.tar.gz rgbds/{include,src,Makefile}
39 cd rgbds 39 cd rgbds
@@ -48,6 +48,11 @@ jobs:
48 # FFmpeg dependencies 48 # FFmpeg dependencies
49 sudo apt-get -y install autoconf automake cmake libtool meson ninja-build pkg-config yasm zlib1g-dev 49 sudo apt-get -y install autoconf automake cmake libtool meson ninja-build pkg-config yasm zlib1g-dev
50 50
51 - name: Build PortMidi (Linux)
52 if: matrix.name == 'Linux'
53 shell: bash
54 run: ./hUGETracker/.github/scripts/build-portmidi.sh
55
51 - name: Install macOS dependencies 56 - name: Install macOS dependencies
52 if: matrix.name == 'Mac' 57 if: matrix.name == 'Mac'
53 run: | 58 run: |
@@ -69,6 +74,11 @@ jobs:
69 mkdir rgbds 74 mkdir rgbds
70 for f in asm link fix; do ln -s `which rgb$f` rgbds/rgb$f; done 75 for f in asm link fix; do ln -s `which rgb$f` rgbds/rgb$f; done
71 76
77 - name: Build PortMidi (Mac)
78 if: matrix.name == 'Mac'
79 shell: bash
80 run: ./hUGETracker/.github/scripts/build-portmidi.sh
81
72 - name: Install Windows dependencies 82 - name: Install Windows dependencies
73 if: matrix.name == 'Windows' 83 if: matrix.name == 'Windows'
74 shell: bash 84 shell: bash
@@ -91,6 +101,11 @@ jobs:
91 mingw-w64-x86_64-toolchain 101 mingw-w64-x86_64-toolchain
92 yasm 102 yasm
93 103
104 - name: Build PortMidi (Windows)
105 if: matrix.name == 'Windows'
106 shell: msys2 {0}
107 run: ./hUGETracker/.github/scripts/build-portmidi.sh
108
94 - name: Compile FFmpeg (Windows) 109 - name: Compile FFmpeg (Windows)
95 if: matrix.name == 'Windows' 110 if: matrix.name == 'Windows'
96 shell: msys2 {0} 111 shell: msys2 {0}
@@ -137,6 +152,7 @@ jobs:
137 run: | 152 run: |
138 unzip -d packaging sdl2.zip SDL2.dll 153 unzip -d packaging sdl2.zip SDL2.dll
139 cp -v ffmpeg-4.4/ffmpeg.exe rgbds/* packaging 154 cp -v ffmpeg-4.4/ffmpeg.exe rgbds/* packaging
155 cp -v portmidi-install/bin/libportmidi.dll packaging/portmidi.dll
140 156
141 - name: Extra Mac packaging 157 - name: Extra Mac packaging
142 if: matrix.name == 'Mac' 158 if: matrix.name == 'Mac'
@@ -147,6 +163,9 @@ jobs:
147 mv packaging/{halt.gb,halt.sym,hUGEDriver,PixeliteTTF.ttf} packaging/hUGETracker.app/Contents/Resources 163 mv packaging/{halt.gb,halt.sym,hUGEDriver,PixeliteTTF.ttf} packaging/hUGETracker.app/Contents/Resources
148 for f in asm link fix; do ln -s `which rgb$f` packaging/hUGETracker.app/Contents/MacOS/rgb$f; done 164 for f in asm link fix; do ln -s `which rgb$f` packaging/hUGETracker.app/Contents/MacOS/rgb$f; done
149 cp -v ffmpeg-4.4/ffmpeg packaging/hUGETracker.app/Contents/MacOS 165 cp -v ffmpeg-4.4/ffmpeg packaging/hUGETracker.app/Contents/MacOS
166 if [ -f portmidi-install/lib/libportmidi.dylib ]; then
167 cp -Rv portmidi-install/lib/libportmidi*.dylib packaging/hUGETracker.app/Contents/MacOS/
168 fi
150 169
151 # Hacky, but unlikely to break any time soon. 170 # Hacky, but unlikely to break any time soon.
152 ed packaging/hUGETracker.app/Contents/Info.plist << END 171 ed packaging/hUGETracker.app/Contents/Info.plist << END
@@ -164,6 +183,9 @@ jobs:
164 run: | 183 run: |
165 for f in asm link fix; do cp -v rgbds/rgb$f packaging/rgb$f; done 184 for f in asm link fix; do cp -v rgbds/rgb$f packaging/rgb$f; done
166 cp -v ffmpeg-4.4/ffmpeg packaging 185 cp -v ffmpeg-4.4/ffmpeg packaging
186 # Bundle portmidi so users don't need libportmidi-dev installed.
187 # We ship both versioned and unversioned names so dlopen finds it.
188 cp -Pv portmidi-install/lib/libportmidi.so* packaging/
167 189
168 - name: Zip files (preserving permissions) 190 - name: Zip files (preserving permissions)
169 shell: bash 191 shell: bash
Modifiedsetup-linux.sh +2−2
@@ -8,8 +8,8 @@ mkdir -p $BUILD_DIR
8 cp fonts/PixeliteTTF.ttf $BUILD_DIR/ 8 cp fonts/PixeliteTTF.ttf $BUILD_DIR/
9 9
10 # Compile halt.gb 10 # Compile halt.gb
11 rgbasm -H -E -I src/hUGEDriver -o hUGEDriver.obj src/hUGEDriver/hUGEDriver.asm 11 rgbasm -E -I src/hUGEDriver -o hUGEDriver.obj src/hUGEDriver/hUGEDriver.asm
12 rgbasm -H -I src/hUGEDriver/include -o halt.obj src/halt.asm 12 rgbasm -I src/hUGEDriver/include -o halt.obj src/halt.asm
13 rgblink -o $BUILD_DIR/halt.gb -n $BUILD_DIR/halt.sym halt.obj hUGEDriver.obj 13 rgblink -o $BUILD_DIR/halt.gb -n $BUILD_DIR/halt.sym halt.obj hUGEDriver.obj
14 rgbfix -vp0xFF $BUILD_DIR/halt.gb 14 rgbfix -vp0xFF $BUILD_DIR/halt.gb
15 15
Modifiedsrc/hUGETracker.lpi +11−1
@@ -203,7 +203,7 @@
203 <PackageName Value="LCL"/> 203 <PackageName Value="LCL"/>
204 </Item5> 204 </Item5>
205 </RequiredPackages> 205 </RequiredPackages>
206 <Units Count="29"> 206 <Units Count="31">
207 <Unit0> 207 <Unit0>
208 <Filename Value="hUGETracker.lpr"/> 208 <Filename Value="hUGETracker.lpr"/>
209 <IsPartOfProject Value="True"/> 209 <IsPartOfProject Value="True"/>
@@ -356,6 +356,16 @@
356 <IsPartOfProject Value="True"/> 356 <IsPartOfProject Value="True"/>
357 <UnitName Value="FurImport"/> 357 <UnitName Value="FurImport"/>
358 </Unit28> 358 </Unit28>
359 <Unit29>
360 <Filename Value="portmidi.pas"/>
361 <IsPartOfProject Value="True"/>
362 <UnitName Value="portmidi"/>
363 </Unit29>
364 <Unit30>
365 <Filename Value="midiinput.pas"/>
366 <IsPartOfProject Value="True"/>
367 <UnitName Value="MidiInput"/>
368 </Unit30>
359 </Units> 369 </Units>
360 </ProjectOptions> 370 </ProjectOptions>
361 <CompilerOptions> 371 <CompilerOptions>
Modifiedsrc/hugesettings.pas +21−0
@@ -24,6 +24,9 @@ type
24 FPatternEditorFontSize: Integer; 24 FPatternEditorFontSize: Integer;
25 FUseScopes, FUseCustomKeymap: Boolean; 25 FUseScopes, FUseCustomKeymap: Boolean;
26 26
27 FMIDIInputEnabled: Boolean;
28 FMIDIInputDevice: String;
29
27 procedure SetDisplayOrderRowNumbersAsHex(AValue: Boolean); 30 procedure SetDisplayOrderRowNumbersAsHex(AValue: Boolean);
28 procedure SetDisplayRowNumbersAsHex(AValue: Boolean); 31 procedure SetDisplayRowNumbersAsHex(AValue: Boolean);
29 procedure SetDrawWaveformGrid(AValue: Boolean); 32 procedure SetDrawWaveformGrid(AValue: Boolean);
@@ -33,6 +36,8 @@ type
33 procedure SetPreviewWhenPlacing(AValue: Boolean); 36 procedure SetPreviewWhenPlacing(AValue: Boolean);
34 procedure SetUseCustomKeymap(AValue: Boolean); 37 procedure SetUseCustomKeymap(AValue: Boolean);
35 procedure SetUseScopes(AValue: Boolean); 38 procedure SetUseScopes(AValue: Boolean);
39 procedure SetMIDIInputEnabled(AValue: Boolean);
40 procedure SetMIDIInputDevice(const AValue: String);
36 public 41 public
37 property PatternEditorFontSize: Integer read FPatternEditorFontSize write SetPatternEditorFontSize; 42 property PatternEditorFontSize: Integer read FPatternEditorFontSize write SetPatternEditorFontSize;
38 property UseScopes: Boolean read FUseScopes write SetUseScopes; 43 property UseScopes: Boolean read FUseScopes write SetUseScopes;
@@ -43,6 +48,8 @@ type
43 property DisplayOrderRowNumbersAsHex: Boolean read FDisplayOrderRowNumbersAsHex write SetDisplayOrderRowNumbersAsHex; 48 property DisplayOrderRowNumbersAsHex: Boolean read FDisplayOrderRowNumbersAsHex write SetDisplayOrderRowNumbersAsHex;
44 property DrawWaveformGrid: Boolean read FDrawWaveformGrid write SetDrawWaveformGrid; 49 property DrawWaveformGrid: Boolean read FDrawWaveformGrid write SetDrawWaveformGrid;
45 property VerticalTabs: Boolean read FVerticalTabs write SetVerticalTabs; 50 property VerticalTabs: Boolean read FVerticalTabs write SetVerticalTabs;
51 property MIDIInputEnabled: Boolean read FMIDIInputEnabled write SetMIDIInputEnabled;
52 property MIDIInputDevice: String read FMIDIInputDevice write SetMIDIInputDevice;
46 53
47 constructor Create; 54 constructor Create;
48 end; 55 end;
@@ -172,6 +179,18 @@ begin
172 SettingsFile.WriteBool('hUGETracker', 'ScopesOn', AValue); 179 SettingsFile.WriteBool('hUGETracker', 'ScopesOn', AValue);
173 end; 180 end;
174 181
182 procedure TTrackerSettings.SetMIDIInputEnabled(AValue: Boolean);
183 begin
184 FMIDIInputEnabled := AValue;
185 SettingsFile.WriteBool('hUGETracker', 'MIDIInputEnabled', AValue);
186 end;
187
188 procedure TTrackerSettings.SetMIDIInputDevice(const AValue: String);
189 begin
190 FMIDIInputDevice := AValue;
191 SettingsFile.WriteString('hUGETracker', 'MIDIInputDevice', AValue);
192 end;
193
175 constructor TTrackerSettings.Create; 194 constructor TTrackerSettings.Create;
176 begin 195 begin
177 SettingsFile := TINIFile.Create(ConcatPaths([ConfDir, 'options.ini'])); 196 SettingsFile := TINIFile.Create(ConcatPaths([ConfDir, 'options.ini']));
@@ -185,6 +204,8 @@ begin
185 FDisplayOrderRowNumbersAsHex := SettingsFile.ReadBool('hUGETracker', 'DisplayOrderRowNumbersAsHex', False); 204 FDisplayOrderRowNumbersAsHex := SettingsFile.ReadBool('hUGETracker', 'DisplayOrderRowNumbersAsHex', False);
186 FDrawWaveformGrid := SettingsFile.ReadBool('hUGETracker', 'DrawWaveformGrid', False); 205 FDrawWaveformGrid := SettingsFile.ReadBool('hUGETracker', 'DrawWaveformGrid', False);
187 FVerticalTabs := SettingsFile.ReadBool('hUGETracker', 'VerticalTabs', False); 206 FVerticalTabs := SettingsFile.ReadBool('hUGETracker', 'VerticalTabs', False);
207 FMIDIInputEnabled := SettingsFile.ReadBool('hUGETracker', 'MIDIInputEnabled', False);
208 FMIDIInputDevice := SettingsFile.ReadString('hUGETracker', 'MIDIInputDevice', '');
188 end; 209 end;
189 210
190 finalization 211 finalization
Addedsrc/midiinput.pas +245−0
@@ -0,0 +1,245 @@
1 unit MidiInput;
2
3 {
4 Cross-platform MIDI input for hUGETracker using PortMidi.
5
6 This unit owns a single global TMidiInput instance, hosts a TTimer that
7 polls the currently open MIDI input stream on the main thread (so callers
8 don't have to worry about synchronizing with the LCL), and raises
9 OnNoteOn / OnNoteOff events with hUGETracker-scaled note numbers.
10
11 Notes are translated from MIDI's 0..127 range to hUGETracker's 0..71
12 (C_3..B_8). MIDI C3 (48) maps to hUGETracker C_3 (0); anything outside
13 the tracker's range is silently dropped so we never feed an out-of-bounds
14 value into the preview code.
15 }
16
17 {$mode objfpc}{$H+}
18
19 interface
20
21 uses
22 Classes, SysUtils, ExtCtrls, portmidi;
23
24 const
25 HT_MIDI_NOTE_OFFSET = 48; // MIDI C3 -> hUGETracker C_3 (note index 0)
26
27 type
28 TMidiNoteEvent = procedure(Sender: TObject; Note, Velocity: Integer) of object;
29
30 TMidiInput = class
31 private
32 FStream: PPortMidiStream;
33 FTimer: TTimer;
34 FDeviceName: String;
35 FOnNoteOn: TMidiNoteEvent;
36 FOnNoteOff: TMidiNoteEvent;
37 FInitialized: Boolean;
38 FAvailable: Boolean;
39 procedure HandleTimer(Sender: TObject);
40 procedure DispatchMessage(msg: PmMessage);
41 public
42 constructor Create;
43 destructor Destroy; override;
44
45 function Available: Boolean;
46 procedure EnumerateInputDevices(Names: TStrings);
47 function FindInputDeviceID(const Name: String): PmDeviceID;
48 function OpenDevice(const Name: String): Boolean;
49 procedure CloseDevice;
50 function IsOpen: Boolean;
51 property DeviceName: String read FDeviceName;
52
53 property OnNoteOn: TMidiNoteEvent read FOnNoteOn write FOnNoteOn;
54 property OnNoteOff: TMidiNoteEvent read FOnNoteOff write FOnNoteOff;
55 end;
56
57 function Midi: TMidiInput;
58
59 // Translate raw MIDI (0..127) to hUGETracker note index. Returns -1 if
60 // the note is outside the tracker's range.
61 function MidiNoteToTrackerNote(MidiNote: Integer): Integer;
62
63 implementation
64
65 var
66 GMidi: TMidiInput = nil;
67
68 function Midi: TMidiInput;
69 begin
70 if GMidi = nil then
71 GMidi := TMidiInput.Create;
72 Result := GMidi;
73 end;
74
75 function MidiNoteToTrackerNote(MidiNote: Integer): Integer;
76 begin
77 Result := MidiNote - HT_MIDI_NOTE_OFFSET;
78 if (Result < 0) or (Result > 71) then
79 Result := -1;
80 end;
81
82 { TMidiInput }
83
84 constructor TMidiInput.Create;
85 begin
86 inherited Create;
87 FStream := nil;
88 FAvailable := LoadPortMidi;
89 if FAvailable then begin
90 if Pm_Initialize() = pmNoError then
91 FInitialized := True
92 else
93 FAvailable := False;
94 end;
95
96 FTimer := TTimer.Create(nil);
97 FTimer.Interval := 5;
98 FTimer.Enabled := False;
99 FTimer.OnTimer := @HandleTimer;
100 end;
101
102 destructor TMidiInput.Destroy;
103 begin
104 CloseDevice;
105 FTimer.Free;
106 if FInitialized then
107 Pm_Terminate();
108 inherited Destroy;
109 end;
110
111 function TMidiInput.Available: Boolean;
112 begin
113 Result := FAvailable;
114 end;
115
116 procedure TMidiInput.EnumerateInputDevices(Names: TStrings);
117 var
118 I, N: Integer;
119 Info: PPmDeviceInfo;
120 begin
121 Names.Clear;
122 if not FAvailable then Exit;
123
124 N := Pm_CountDevices();
125 for I := 0 to N - 1 do begin
126 Info := Pm_GetDeviceInfo(I);
127 if (Info <> nil) and (Info^.input <> 0) then
128 Names.Add(String(Info^.name));
129 end;
130 end;
131
132 function TMidiInput.FindInputDeviceID(const Name: String): PmDeviceID;
133 var
134 I, N: Integer;
135 Info: PPmDeviceInfo;
136 begin
137 Result := pmNoDevice;
138 if (not FAvailable) or (Name = '') then Exit;
139
140 N := Pm_CountDevices();
141 for I := 0 to N - 1 do begin
142 Info := Pm_GetDeviceInfo(I);
143 if (Info <> nil) and (Info^.input <> 0) and (String(Info^.name) = Name) then
144 Exit(I);
145 end;
146 end;
147
148 function TMidiInput.OpenDevice(const Name: String): Boolean;
149 var
150 DevID: PmDeviceID;
151 begin
152 Result := False;
153 if not FAvailable then Exit;
154
155 CloseDevice;
156
157 DevID := FindInputDeviceID(Name);
158 if DevID = pmNoDevice then Exit;
159
160 if Pm_OpenInput(FStream, DevID, nil, 256, nil, nil) <> pmNoError then begin
161 FStream := nil;
162 Exit;
163 end;
164
165 FDeviceName := Name;
166 FTimer.Enabled := True;
167 Result := True;
168 end;
169
170 procedure TMidiInput.CloseDevice;
171 begin
172 FTimer.Enabled := False;
173 if FStream <> nil then begin
174 Pm_Close(FStream);
175 FStream := nil;
176 end;
177 FDeviceName := '';
178 end;
179
180 function TMidiInput.IsOpen: Boolean;
181 begin
182 Result := FStream <> nil;
183 end;
184
185 procedure TMidiInput.DispatchMessage(msg: PmMessage);
186 var
187 Status, Data1, Data2: Byte;
188 Kind, TrackerNote: Integer;
189 begin
190 Status := Pm_MessageStatus(msg);
191 Data1 := Pm_MessageData1(msg);
192 Data2 := Pm_MessageData2(msg);
193
194 Kind := Status and $F0;
195 TrackerNote := MidiNoteToTrackerNote(Data1);
196 if TrackerNote < 0 then Exit;
197
198 case Kind of
199 $90: // Note On
200 begin
201 // MIDI convention: Note On with velocity 0 is a Note Off.
202 if Data2 = 0 then begin
203 if Assigned(FOnNoteOff) then FOnNoteOff(Self, TrackerNote, 0);
204 end
205 else begin
206 if Assigned(FOnNoteOn) then FOnNoteOn(Self, TrackerNote, Data2);
207 end;
208 end;
209 $80: // Note Off
210 if Assigned(FOnNoteOff) then FOnNoteOff(Self, TrackerNote, Data2);
211 end;
212 end;
213
214 procedure TMidiInput.HandleTimer(Sender: TObject);
215 const
216 BUF_SIZE = 32;
217 var
218 Buf: array[0..BUF_SIZE-1] of TPmEvent;
219 Count, I: Integer;
220 PollResult: PmError;
221 begin
222 if FStream = nil then Exit;
223
224 PollResult := Pm_Poll(FStream);
225 if PollResult < 0 then begin
226 // Device likely removed - close cleanly so we stop hammering it.
227 CloseDevice;
228 Exit;
229 end;
230
231 while Pm_Poll(FStream) = pmGotData do begin
232 Count := Pm_Read(FStream, @Buf[0], BUF_SIZE);
233 if Count <= 0 then Break;
234 for I := 0 to Count - 1 do
235 DispatchMessage(Buf[I].message_);
236 end;
237 end;
238
239 finalization
240 if GMidi <> nil then begin
241 GMidi.Free;
242 GMidi := nil;
243 end;
244
245 end.
Modifiedsrc/options.lfm +50−0
@@ -177,6 +177,56 @@ object frmOptions: TfrmOptions
177 OnChange = KeymapCheckboxChange 177 OnChange = KeymapCheckboxChange
178 end 178 end
179 end 179 end
180 object MIDITabSheet: TTabSheet
181 Caption = 'MIDI'
182 ClientHeight = 840
183 ClientWidth = 701
184 object MIDIEnabledCheckbox: TCheckBox
185 Left = 21
186 Height = 25
187 Top = 21
188 Width = 269
189 Caption = 'Enable MIDI keyboard input'
190 TabOrder = 0
191 OnChange = MIDIEnabledCheckboxChange
192 end
193 object MIDIDeviceLabel: TLabel
194 Left = 21
195 Height = 23
196 Top = 64
197 Width = 119
198 Caption = 'Input device:'
199 ParentColor = False
200 end
201 object MIDIDeviceComboBox: TComboBox
202 Left = 21
203 Height = 33
204 Top = 93
205 Width = 512
206 ItemHeight = 0
207 Style = csDropDownList
208 TabOrder = 1
209 OnChange = MIDIDeviceComboBoxChange
210 end
211 object MIDIRefreshButton: TButton
212 Left = 543
213 Height = 33
214 Top = 93
215 Width = 120
216 Caption = 'Refresh'
217 TabOrder = 2
218 OnClick = MIDIRefreshButtonClick
219 end
220 object MIDIStatusLabel: TLabel
221 Left = 21
222 Height = 23
223 Top = 140
224 Width = 600
225 Caption = ''
226 ParentColor = False
227 WordWrap = True
228 end
229 end
180 object CustomiztaionTabSheet: TTabSheet 230 object CustomiztaionTabSheet: TTabSheet
181 Caption = 'Appearance' 231 Caption = 'Appearance'
182 ClientHeight = 840 232 ClientHeight = 840
Modifiedsrc/options.pas +90−1
@@ -7,7 +7,7 @@ interface
7 uses 7 uses
8 Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, Spin, Grids, 8 Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, Spin, Grids,
9 LCLProc, Buttons, ExtCtrls, ComCtrls, CheckLst, Constants, Keymap, 9 LCLProc, Buttons, ExtCtrls, ComCtrls, CheckLst, Constants, Keymap,
10 hUGESettings, hUGEDataTypes, TrackerGrid; 10 hUGESettings, hUGEDataTypes, TrackerGrid, MidiInput;
11 11
12 type 12 type
13 13
@@ -42,6 +42,12 @@ type
42 KeyboardTabSheet: TTabSheet; 42 KeyboardTabSheet: TTabSheet;
43 GeneralTabSheet: TTabSheet; 43 GeneralTabSheet: TTabSheet;
44 CustomiztaionTabSheet: TTabSheet; 44 CustomiztaionTabSheet: TTabSheet;
45 MIDITabSheet: TTabSheet;
46 MIDIEnabledCheckbox: TCheckBox;
47 MIDIDeviceComboBox: TComboBox;
48 MIDIRefreshButton: TButton;
49 MIDIDeviceLabel: TLabel;
50 MIDIStatusLabel: TLabel;
45 VolumeEffectTextColorButton: TColorButton; 51 VolumeEffectTextColorButton: TColorButton;
46 PanningEffectTextColorButton: TColorButton; 52 PanningEffectTextColorButton: TColorButton;
47 SongEffectTextColorButton: TColorButton; 53 SongEffectTextColorButton: TColorButton;
@@ -86,11 +92,17 @@ type
86 procedure KeyMapStringGridValidateEntry(sender: TObject; aCol, aRow: Integer; 92 procedure KeyMapStringGridValidateEntry(sender: TObject; aCol, aRow: Integer;
87 const OldValue: string; var NewValue: String); 93 const OldValue: string; var NewValue: String);
88 procedure NoteTextColorButtonColorChanged(Sender: TObject); 94 procedure NoteTextColorButtonColorChanged(Sender: TObject);
95 procedure MIDIEnabledCheckboxChange(Sender: TObject);
96 procedure MIDIDeviceComboBoxChange(Sender: TObject);
97 procedure MIDIRefreshButtonClick(Sender: TObject);
89 private 98 private
90 SamplePatternMap: TPatternMap; 99 SamplePatternMap: TPatternMap;
91 SampleTrackerGrid: TTrackerGrid; 100 SampleTrackerGrid: TTrackerGrid;
92 LoadingColors: Boolean; 101 LoadingColors: Boolean;
102 LoadingMidiUI: Boolean;
93 103
104 procedure RefreshMidiDevices;
105 procedure UpdateMidiStatus;
94 procedure RecreateTrackerGrid; 106 procedure RecreateTrackerGrid;
95 procedure UpdateTrackerGridColors; 107 procedure UpdateTrackerGridColors;
96 procedure SaveColorsToFile(Filename: String); 108 procedure SaveColorsToFile(Filename: String);
@@ -189,6 +201,10 @@ begin
189 TrackerSettings.DisplayOrderRowNumbersAsHex := CheckListBox1.Checked[4]; 201 TrackerSettings.DisplayOrderRowNumbersAsHex := CheckListBox1.Checked[4];
190 TrackerSettings.DrawWaveformGrid := CheckListBox1.Checked[5]; 202 TrackerSettings.DrawWaveformGrid := CheckListBox1.Checked[5];
191 TrackerSettings.VerticalTabs := CheckListBox1.Checked[6]; 203 TrackerSettings.VerticalTabs := CheckListBox1.Checked[6];
204
205 TrackerSettings.MIDIInputEnabled := MIDIEnabledCheckbox.Checked;
206 if MIDIDeviceComboBox.ItemIndex >= 0 then
207 TrackerSettings.MIDIInputDevice := MIDIDeviceComboBox.Items[MIDIDeviceComboBox.ItemIndex];
192 end; 208 end;
193 209
194 procedure TfrmOptions.FormCreate(Sender: TObject); 210 procedure TfrmOptions.FormCreate(Sender: TObject);
@@ -226,6 +242,79 @@ begin
226 end; 242 end;
227 243
228 RecreateTrackerGrid; 244 RecreateTrackerGrid;
245
246 LoadingMidiUI := True;
247 MIDIEnabledCheckbox.Checked := TrackerSettings.MIDIInputEnabled;
248 RefreshMidiDevices;
249 LoadingMidiUI := False;
250 UpdateMidiStatus;
251 end;
252
253 procedure TfrmOptions.RefreshMidiDevices;
254 var
255 Idx: Integer;
256 begin
257 MIDIDeviceComboBox.Items.BeginUpdate;
258 try
259 MIDIDeviceComboBox.Items.Clear;
260 if Midi.Available then
261 Midi.EnumerateInputDevices(MIDIDeviceComboBox.Items);
262
263 Idx := MIDIDeviceComboBox.Items.IndexOf(TrackerSettings.MIDIInputDevice);
264 if Idx >= 0 then
265 MIDIDeviceComboBox.ItemIndex := Idx
266 else if MIDIDeviceComboBox.Items.Count > 0 then
267 MIDIDeviceComboBox.ItemIndex := 0
268 else
269 MIDIDeviceComboBox.ItemIndex := -1;
270 finally
271 MIDIDeviceComboBox.Items.EndUpdate;
272 end;
273
274 MIDIDeviceComboBox.Enabled := Midi.Available and MIDIEnabledCheckbox.Checked;
275 MIDIRefreshButton.Enabled := Midi.Available;
276 end;
277
278 procedure TfrmOptions.UpdateMidiStatus;
279 begin
280 if not Midi.Available then
281 MIDIStatusLabel.Caption :=
282 'PortMidi is not installed or could not be loaded. Install libportmidi and restart hUGETracker to enable MIDI input.'
283 else if MIDIDeviceComboBox.Items.Count = 0 then
284 MIDIStatusLabel.Caption := 'No MIDI input devices detected.'
285 else
286 MIDIStatusLabel.Caption := '';
287 end;
288
289 procedure TfrmOptions.MIDIEnabledCheckboxChange(Sender: TObject);
290 begin
291 if LoadingMidiUI then Exit;
292 TrackerSettings.MIDIInputEnabled := MIDIEnabledCheckbox.Checked;
293 MIDIDeviceComboBox.Enabled := Midi.Available and MIDIEnabledCheckbox.Checked;
294 if not MIDIEnabledCheckbox.Checked then
295 Midi.CloseDevice
296 else if (MIDIDeviceComboBox.ItemIndex >= 0) and Midi.Available then
297 Midi.OpenDevice(MIDIDeviceComboBox.Items[MIDIDeviceComboBox.ItemIndex]);
298 end;
299
300 procedure TfrmOptions.MIDIDeviceComboBoxChange(Sender: TObject);
301 begin
302 if LoadingMidiUI then Exit;
303 if MIDIDeviceComboBox.ItemIndex < 0 then Exit;
304 TrackerSettings.MIDIInputDevice := MIDIDeviceComboBox.Items[MIDIDeviceComboBox.ItemIndex];
305 if TrackerSettings.MIDIInputEnabled and Midi.Available then
306 Midi.OpenDevice(TrackerSettings.MIDIInputDevice);
307 end;
308
309 procedure TfrmOptions.MIDIRefreshButtonClick(Sender: TObject);
310 begin
311 LoadingMidiUI := True;
312 try
313 RefreshMidiDevices;
314 finally
315 LoadingMidiUI := False;
316 end;
317 UpdateMidiStatus;
229 end; 318 end;
230 319
231 procedure TfrmOptions.KeymapCheckboxChange(Sender: TObject); 320 procedure TfrmOptions.KeymapCheckboxChange(Sender: TObject);
Addedsrc/portmidi.pas +213−0
@@ -0,0 +1,213 @@
1 unit portmidi;
2
3 {
4 Minimal Pascal bindings for PortMidi (https://github.com/PortMidi/portmidi).
5
6 Only the input-side of the API is translated - that's all hUGETracker needs.
7 PortMidi is loaded dynamically via the dynlibs unit so the application still
8 runs on machines where the shared library isn't installed; callers should
9 check LoadPortMidi before touching any of the Pm_* routines.
10
11 Calling convention is cdecl on every supported platform (PortMidi's public
12 header uses PMEXPORT which is either empty or __declspec; there is no
13 __stdcall). PortTime is not bound here; passing nil for the time_proc /
14 time_info parameters of Pm_OpenInput makes PortMidi use its own internal
15 millisecond clock, which is all we need for note input.
16 }
17
18 {$mode objfpc}{$H+}
19
20 interface
21
22 uses
23 ctypes, dynlibs, SysUtils;
24
25 const
26 {$if defined(MSWINDOWS)}
27 PortMidiLibName = 'portmidi.dll';
28 {$elseif defined(DARWIN)}
29 PortMidiLibName = 'libportmidi.dylib';
30 {$else}
31 PortMidiLibName = 'libportmidi.so.2';
32 {$endif}
33
34 pmNoError = 0;
35 pmNoData = 0;
36 pmGotData = 1;
37 pmHostError = -10000;
38 pmInvalidDeviceId = -9999;
39 pmInsufficientMemory = -9998;
40 pmBufferTooSmall = -9997;
41 pmBufferOverflow = -9996;
42 pmBadPtr = -9995;
43 pmBadData = -9994;
44 pmInternalError = -9993;
45 pmBufferMaxSize = -9992;
46 pmNotImplemented = -9991;
47 pmInterfaceNotSupported = -9990;
48 pmNameConflict = -9989;
49 pmDeviceRemoved = -9988;
50
51 pmNoDevice = -1;
52
53 type
54 PmError = cint32;
55 PmDeviceID = cint32;
56 PmTimestamp = cint32;
57 PmMessage = cuint32;
58 PPortMidiStream = Pointer;
59 PPPortMidiStream = ^PPortMidiStream;
60 PmTimeProcPtr = function(time_info: Pointer): PmTimestamp; cdecl;
61
62 TPmDeviceInfo = record
63 structVersion: cint;
64 interf: PAnsiChar;
65 name: PAnsiChar;
66 input: cint;
67 output: cint;
68 opened: cint;
69 is_virtual: cint;
70 end;
71 PPmDeviceInfo = ^TPmDeviceInfo;
72
73 TPmEvent = record
74 message_: PmMessage;
75 timestamp: PmTimestamp;
76 end;
77 PPmEvent = ^TPmEvent;
78
79 var
80 Pm_Initialize: function: PmError; cdecl;
81 Pm_Terminate: function: PmError; cdecl;
82 Pm_HasHostError: function(stream: PPortMidiStream): cint; cdecl;
83 Pm_GetErrorText: function(errnum: PmError): PAnsiChar; cdecl;
84 Pm_GetHostErrorText: procedure(msg: PAnsiChar; len: cuint); cdecl;
85 Pm_CountDevices: function: cint; cdecl;
86 Pm_GetDefaultInputDeviceID: function: PmDeviceID; cdecl;
87 Pm_GetDefaultOutputDeviceID: function: PmDeviceID; cdecl;
88 Pm_GetDeviceInfo: function(id: PmDeviceID): PPmDeviceInfo; cdecl;
89 Pm_OpenInput: function(out stream: PPortMidiStream;
90 inputDevice: PmDeviceID;
91 inputSysDepInfo: Pointer;
92 bufferSize: cint32;
93 time_proc: PmTimeProcPtr;
94 time_info: Pointer): PmError; cdecl;
95 Pm_SetFilter: function(stream: PPortMidiStream; filters: cint32): PmError; cdecl;
96 Pm_SetChannelMask: function(stream: PPortMidiStream; mask: cint): PmError; cdecl;
97 Pm_Abort: function(stream: PPortMidiStream): PmError; cdecl;
98 Pm_Close: function(stream: PPortMidiStream): PmError; cdecl;
99 Pm_Read: function(stream: PPortMidiStream; buffer: PPmEvent; length: cint32): cint; cdecl;
100 Pm_Poll: function(stream: PPortMidiStream): PmError; cdecl;
101
102 function LoadPortMidi: Boolean;
103 procedure UnloadPortMidi;
104 function IsPortMidiLoaded: Boolean;
105
106 function Pm_MessageStatus(msg: PmMessage): Byte; inline;
107 function Pm_MessageData1(msg: PmMessage): Byte; inline;
108 function Pm_MessageData2(msg: PmMessage): Byte; inline;
109
110 implementation
111
112 var
113 LibHandle: TLibHandle = NilHandle;
114
115 function IsPortMidiLoaded: Boolean;
116 begin
117 Result := LibHandle <> NilHandle;
118 end;
119
120 function Pm_MessageStatus(msg: PmMessage): Byte;
121 begin
122 Result := Byte(msg and $FF);
123 end;
124
125 function Pm_MessageData1(msg: PmMessage): Byte;
126 begin
127 Result := Byte((msg shr 8) and $FF);
128 end;
129
130 function Pm_MessageData2(msg: PmMessage): Byte;
131 begin
132 Result := Byte((msg shr 16) and $FF);
133 end;
134
135 function ResolveSym(const Name: String): Pointer;
136 begin
137 Result := GetProcedureAddress(LibHandle, Name);
138 if Result = nil then begin
139 UnloadLibrary(LibHandle);
140 LibHandle := NilHandle;
141 end;
142 end;
143
144 function TryLoad(const Name: String): TLibHandle;
145 begin
146 Result := LoadLibrary(Name);
147 end;
148
149 function LoadPortMidi: Boolean;
150 var
151 ExeDir: String;
152 begin
153 if LibHandle <> NilHandle then
154 Exit(True);
155
156 LibHandle := TryLoad(PortMidiLibName);
157
158 // On non-Windows, dlopen() by filename only does not search the
159 // executable's directory, so explicitly look next to the running binary
160 // before falling back to system paths.
161 ExeDir := ExtractFilePath(ParamStr(0));
162 if (LibHandle = NilHandle) and (ExeDir <> '') then
163 LibHandle := TryLoad(ExeDir + PortMidiLibName);
164
165 {$ifdef LINUX}
166 if LibHandle = NilHandle then LibHandle := TryLoad('libportmidi.so');
167 if LibHandle = NilHandle then LibHandle := TryLoad('libportmidi.so.0');
168 if LibHandle = NilHandle then LibHandle := TryLoad('libportmidi.so.1');
169 if (LibHandle = NilHandle) and (ExeDir <> '') then
170 LibHandle := TryLoad(ExeDir + 'libportmidi.so');
171 {$endif}
172 {$ifdef DARWIN}
173 if LibHandle = NilHandle then LibHandle := TryLoad('libportmidi.2.dylib');
174 if (LibHandle = NilHandle) and (ExeDir <> '') then
175 LibHandle := TryLoad(ExeDir + 'libportmidi.2.dylib');
176 if LibHandle = NilHandle then LibHandle := TryLoad('/usr/local/lib/libportmidi.dylib');
177 if LibHandle = NilHandle then LibHandle := TryLoad('/opt/homebrew/lib/libportmidi.dylib');
178 {$endif}
179
180 if LibHandle = NilHandle then Exit(False);
181
182 Pointer(Pm_Initialize) := ResolveSym('Pm_Initialize');
183 Pointer(Pm_Terminate) := ResolveSym('Pm_Terminate');
184 Pointer(Pm_HasHostError) := ResolveSym('Pm_HasHostError');
185 Pointer(Pm_GetErrorText) := ResolveSym('Pm_GetErrorText');
186 Pointer(Pm_GetHostErrorText) := ResolveSym('Pm_GetHostErrorText');
187 Pointer(Pm_CountDevices) := ResolveSym('Pm_CountDevices');
188 Pointer(Pm_GetDefaultInputDeviceID) := ResolveSym('Pm_GetDefaultInputDeviceID');
189 Pointer(Pm_GetDefaultOutputDeviceID) := ResolveSym('Pm_GetDefaultOutputDeviceID');
190 Pointer(Pm_GetDeviceInfo) := ResolveSym('Pm_GetDeviceInfo');
191 Pointer(Pm_OpenInput) := ResolveSym('Pm_OpenInput');
192 Pointer(Pm_SetFilter) := ResolveSym('Pm_SetFilter');
193 Pointer(Pm_SetChannelMask) := ResolveSym('Pm_SetChannelMask');
194 Pointer(Pm_Abort) := ResolveSym('Pm_Abort');
195 Pointer(Pm_Close) := ResolveSym('Pm_Close');
196 Pointer(Pm_Read) := ResolveSym('Pm_Read');
197 Pointer(Pm_Poll) := ResolveSym('Pm_Poll');
198
199 Result := LibHandle <> NilHandle;
200 end;
201
202 procedure UnloadPortMidi;
203 begin
204 if LibHandle <> NilHandle then begin
205 UnloadLibrary(LibHandle);
206 LibHandle := NilHandle;
207 end;
208 end;
209
210 finalization
211 UnloadPortMidi;
212
213 end.
Modifiedsrc/tracker.pas +91−17
@@ -10,7 +10,8 @@ uses
10 about_hugetracker, TrackerGrid, lclintf, lmessages, Buttons, Grids, DBCtrls, 10 about_hugetracker, TrackerGrid, lclintf, lmessages, Buttons, Grids, DBCtrls,
11 HugeDatatypes, LCLType, Clipbrd, RackCtls, Codegen, SymParser, options, 11 HugeDatatypes, LCLType, Clipbrd, RackCtls, Codegen, SymParser, options,
12 bgrabitmap, effecteditor, RenderToWave, modimport, mainloop, strutils, 12 bgrabitmap, effecteditor, RenderToWave, modimport, mainloop, strutils,
13 Types, Keymap, hUGESettings, vgm, TBMImport, FurImport, InstrumentPreview, findreplace; 13 Types, Keymap, hUGESettings, vgm, TBMImport, FurImport, InstrumentPreview, findreplace,
14 MidiInput;
14 15
15 // TODO: Move to config file? 16 // TODO: Move to config file?
16 const 17 const
@@ -541,9 +542,19 @@ type
541 procedure PreviewInstrument(Note: Integer; Instr: TInstrument; SquareOnCh2: Boolean = False); overload; 542 procedure PreviewInstrument(Note: Integer; Instr: TInstrument; SquareOnCh2: Boolean = False); overload;
542 procedure PreviewNote(Note: Integer); 543 procedure PreviewNote(Note: Integer);
543 procedure Panic; 544 procedure Panic;
545
546 procedure OnMidiNoteOn(Sender: TObject; Note, Velocity: Integer);
547 procedure OnMidiNoteOff(Sender: TObject; Note, Velocity: Integer);
548
549 // Shared between the computer-keyboard preview path (FormKeyDown) and
550 // MIDI note-on. Re-points InstrumentComboBox at the current channel's
551 // bank, then previews the note. Returns False if the note shouldn't
552 // actually be played (no instrument selected, unknown note, etc.).
553 function PreviewForNoteEntry(Note: Integer): Boolean;
544 public 554 public
545 procedure OnTrackerGridResize(Sender: TObject); 555 procedure OnTrackerGridResize(Sender: TObject);
546 procedure OnTrackerGridCursorOutOfBounds; 556 procedure OnTrackerGridCursorOutOfBounds;
557 procedure ApplyMidiSettings;
547 end; 558 end;
548 559
549 var 560 var
@@ -928,6 +939,53 @@ begin
928 UnlockPlayback; 939 UnlockPlayback;
929 end; 940 end;
930 941
942 procedure TfrmTracker.OnMidiNoteOn(Sender: TObject; Note, Velocity: Integer);
943 begin
944 if LoadingFile or Playing then Exit;
945 if (Note < LOWEST_NOTE) or (Note > HIGHEST_NOTE) then Exit;
946
947 // MIDI input mirrors computer-keyboard behaviour: re-aim the instrument
948 // combo box at the current channel's bank and preview through that
949 // instrument, then commit the note into the active cell. PreviewingInstrument
950 // is tracked so the matching Note Off can silence the preview, the same
951 // way FormKeyUp handles a released computer key.
952 if TrackerSettings.PreviewWhenPlacing
953 and (ActiveControl = TrackerGrid)
954 and PreviewForNoteEntry(Note) then
955 PreviewingInstrument := Note
956 else
957 PreviewNote(Note);
958
959 if (ActiveControl = TrackerGrid) and (TrackerGrid.Cursor.SelectedPart = cpNote) then
960 TrackerGrid.InputNoteValue(Note);
961 end;
962
963 procedure TfrmTracker.OnMidiNoteOff(Sender: TObject; Note, Velocity: Integer);
964 begin
965 if PreviewingInstrument <> -1 then begin
966 PreviewingInstrument := -1;
967 Panic;
968 end;
969 end;
970
971 procedure TfrmTracker.ApplyMidiSettings;
972 begin
973 if not TrackerSettings.MIDIInputEnabled then begin
974 if Midi.IsOpen then Midi.CloseDevice;
975 Exit;
976 end;
977
978 if not Midi.Available then Exit;
979
980 Midi.OnNoteOn := @OnMidiNoteOn;
981 Midi.OnNoteOff := @OnMidiNoteOff;
982
983 if TrackerSettings.MIDIInputDevice = '' then Exit;
984
985 if Midi.DeviceName <> TrackerSettings.MIDIInputDevice then
986 Midi.OpenDevice(TrackerSettings.MIDIInputDevice);
987 end;
988
931 procedure TfrmTracker.OnTrackerGridResize(Sender: TObject); 989 procedure TfrmTracker.OnTrackerGridResize(Sender: TObject);
932 var 990 var
933 I: Integer; 991 I: Integer;
@@ -1553,13 +1611,42 @@ begin
1553 LoadSong(ParamStr(1)) 1611 LoadSong(ParamStr(1))
1554 else 1612 else
1555 UpdateUIAfterLoad; 1613 UpdateUIAfterLoad;
1614
1615 ApplyMidiSettings;
1616 end;
1617
1618 function TfrmTracker.PreviewForNoteEntry(Note: Integer): Boolean;
1619 var
1620 Freq: Integer;
1621 begin
1622 Result := False;
1623 if TrackerGrid.Cursor.SelectedPart <> cpNote then Exit;
1624 if InstrumentComboBox.ItemIndex <= 0 then Exit;
1625 if not NotesToFreqs.TryGetData(Note, Freq) then Exit;
1626
1627 // Re-map the current combo-box selection into the bank that matches the
1628 // channel the cursor is on (square / wave / noise), then push that back
1629 // onto TrackerGrid.SelectedInstrument so the note that gets committed
1630 // into the cell picks up the correct instrument number.
1631 case TrackerGrid.Cursor.X of
1632 0..1: InstrumentComboBox.ItemIndex := UnmodInst(itSquare, TrackerGrid.SelectedInstrument);
1633 2: InstrumentComboBox.ItemIndex := UnmodInst(itWave, TrackerGrid.SelectedInstrument);
1634 3: InstrumentComboBox.ItemIndex := UnmodInst(itNoise, TrackerGrid.SelectedInstrument);
1635 end;
1636 TrackerGrid.SelectedInstrument := ModInst(InstrumentComboBox.ItemIndex);
1637
1638 if TrackerGrid.Cursor.X = 1 then
1639 PreviewInstrument(Note, InstrumentComboBox.ItemIndex, True)
1640 else
1641 PreviewInstrument(Note, InstrumentComboBox.ItemIndex, False);
1642
1643 Result := True;
1556 end; 1644 end;
1557 1645
1558 procedure TfrmTracker.FormKeyDown(Sender: TObject; var Key: Word; 1646 procedure TfrmTracker.FormKeyDown(Sender: TObject; var Key: Word;
1559 Shift: TShiftState); 1647 Shift: TShiftState);
1560 var 1648 var
1561 Note: Integer; 1649 Note: Integer;
1562 Freq: Integer;
1563 begin 1650 begin
1564 // Guard conditions 1651 // Guard conditions
1565 if TrackerGrid.Cursor.SelectedPart <> cpNote then Exit; 1652 if TrackerGrid.Cursor.SelectedPart <> cpNote then Exit;
@@ -1574,23 +1661,10 @@ begin
1574 PreviewingInstrument := -1; 1661 PreviewingInstrument := -1;
1575 1662
1576 if (PreviewingInstrument > -1) or 1663 if (PreviewingInstrument > -1) or
1577 (not (ActiveControl = TrackerGrid)) or 1664 (not (ActiveControl = TrackerGrid))
1578 (InstrumentComboBox.ItemIndex <= 0)
1579 then exit; 1665 then exit;
1580 1666
1581 if not NotesToFreqs.TryGetData(Note, Freq) then Exit; 1667 if not PreviewForNoteEntry(Note) then Exit;
1582
1583 case TrackerGrid.Cursor.X of
1584 0..1: InstrumentComboBox.ItemIndex := UnmodInst(itSquare, TrackerGrid.SelectedInstrument);
1585 2: InstrumentComboBox.ItemIndex := UnmodInst(itWave, TrackerGrid.SelectedInstrument);
1586 3: InstrumentComboBox.ItemIndex := UnmodInst(itNoise, TrackerGrid.SelectedInstrument);
1587 end;
1588 TrackerGrid.SelectedInstrument := ModInst(InstrumentComboBox.ItemIndex);
1589
1590 if TrackerGrid.Cursor.X = 1 then
1591 PreviewInstrument(Note, InstrumentComboBox.ItemIndex, True)
1592 else
1593 PreviewInstrument(Note, InstrumentComboBox.ItemIndex, False);
1594 1668
1595 PreviewingInstrument := Note; 1669 PreviewingInstrument := Note;
1596 PreviewingWithKey := Key; 1670 PreviewingWithKey := Key;
Modifiedsrc/trackergrid.pas +21−12
@@ -139,6 +139,8 @@ type
139 procedure DeleteRowInPatternAtCursor(Pattern: Integer); 139 procedure DeleteRowInPatternAtCursor(Pattern: Integer);
140 procedure DeleteRowInAllAtCursor; 140 procedure DeleteRowInAllAtCursor;
141 141
142 procedure InputNoteValue(Note: Integer);
143
142 procedure SelectAll; 144 procedure SelectAll;
143 procedure SelectColumn; 145 procedure SelectColumn;
144 procedure EraseSelection; 146 procedure EraseSelection;
@@ -1027,27 +1029,34 @@ begin
1027 EndUndoAction; 1029 EndUndoAction;
1028 end; 1030 end;
1029 1031
1030 procedure TTrackerGrid.InputNote(Key: Word); 1032 procedure TTrackerGrid.InputNoteValue(Note: Integer);
1031 var 1033 var
1032 Temp: Integer; 1034 Cell: PCell;
1033 begin 1035 begin
1034 BeginUndoAction; 1036 if (Note < LOWEST_NOTE) or (Note > HIGHEST_NOTE) then Exit;
1035 Temp := -1; 1037 if Cursor.SelectedPart <> cpNote then Exit;
1036 1038
1037 with Patterns[Cursor.X]^[Cursor.Y] do 1039 BeginUndoAction;
1038 if Keybindings.TryGetData(Key, Temp) then begin 1040 Cell := @Patterns[Cursor.X]^[Cursor.Y];
1039 Note := Min(HIGHEST_NOTE, Temp+(SelectedOctave*12)); 1041 Cell^.Note := Note;
1040 if SelectedInstrument <> 0 then 1042 if SelectedInstrument <> 0 then
1041 Instrument := SelectedInstrument; 1043 Cell^.Instrument := SelectedInstrument;
1042 1044
1043 Inc(Cursor.Y, Step); 1045 Inc(Cursor.Y, Step);
1044 ClampCursors; 1046 ClampCursors;
1045 end;
1046 1047
1047 Invalidate; 1048 Invalidate;
1048 EndUndoAction; 1049 EndUndoAction;
1049 end; 1050 end;
1050 1051
1052 procedure TTrackerGrid.InputNote(Key: Word);
1053 var
1054 Temp: Integer;
1055 begin
1056 if Keybindings.TryGetData(Key, Temp) then
1057 InputNoteValue(Min(HIGHEST_NOTE, Temp + (SelectedOctave * 12)));
1058 end;
1059
1051 procedure TTrackerGrid.InputInstrument(Key: Word); 1060 procedure TTrackerGrid.InputInstrument(Key: Word);
1052 var 1061 var
1053 Temp: Nibble; 1062 Temp: Nibble;