Sign in Join
Wiki copytemporary

macOS MIDI Player: what changed

Revision 0 to 1. Removed lines have a minus sign; added lines have a plus sign.

Title: macOS MIDI Player

OldNewLine
1+*(macOS only)*
2+
3+## AVMidiPlayer
4+
5+The AVMidiPlayer class lets you play music file formats such as [MIDI](https://www.midi.org/) and iMelody (non-polyphonic sound format created for mobile phones). It is available from macOS 10.10 (Yosemite). The properties of this class are used for managing information about a music file such as the playback point within the sound’s timeline, duration and playback rate.
6+
7+## Example code
8+
9+> **Note:** Requires FPC trunk (3.3.1) and macOS 10.10+ (Yosemite).
10+
11+![](/wiki-images/0/09/Avmidiplayer2.png)
12+
13+The example code below creates a basic MIDI file player application which plays the midi file included in the [[Application Bundle|application bundle]] Resources directory. This is useful in itself since Apple removed the ability to play MIDI files in macOS 10.9 (Mavericks) in 2013. While you could still download QuickTime 7 (a 32 bit application) [from Apple](https://support.apple.com/kb/dl923), macOS 10.15 (Catalina) removed all support for 32 bit software, which includes QuickTime 7 and all media formats and codecs relying on it.
14+
15+``` pascal
16+unit Unit1;
17+
18+{$mode objfpc}{$H+}
19+{$modeswitch objectivec1}
20+{$linkframework AVFoundation}
21+{$modeswitch cblocks}
22+
23+//{$DEFINE DEBUG} // Log completion handler calls to console and terminal
24+
25+{ Requires FPC trunk (3.3.1) and macOS 10.10+ (Yosemite) }
26+
27+interface
28+
29+uses
30+ Classes, SysUtils, Forms, Controls, Graphics, Dialogs,
31+ CocoaAll, LCLType, StdCtrls, ExtCtrls, ComCtrls;
32+
33+type
34+ tblock = reference to procedure; cdecl; cblock;
35+
36+type
37+ { AVMIDIPlayer}
38+
39+ AVMIDIPlayer = objcclass external(NSObject)
40+ public
41+ {Initializes a newly allocated MIDI player with the contents of the file
42+ specified by the URL, using the specified sound bank.
43+ inURL - The file to play.
44+ bankURL - The URL of the sound bank. The sound bank must be a SoundFont2 or DLS bank.
45+ For macOS the bankURL can be set to nil to use the default sound bank.
46+ outError - Returns, by-reference, a description of the error, if an error occurs.}
47+ function initWithContentsOfURL_error (inURL: NSURL; soundBankURL: NSURL; outError: NSErrorPtr): id;
48+ message 'initWithContentsOfURL:soundBankURL:error:';
49+ {Prepares to play the sequence by prerolling all events.
50+ Happens automatically on play if it has not already been called, but may produce a
51+ delay in startup.}
52+ procedure prepareToPlay; message 'prepareToPlay';
53+ {Plays the sequence.
54+ completionHandler - A block that is executed when playback is completed or stopped.
55+ If prepareToPlay has not been invoked, play may be delayed while the events are prerolled.}
56+ //procedure play (completionHandler: AVMIDIPlayerCompletionHandler); message 'play:';
57+ procedure play (completionHandler: tblock); message 'play:';
58+ {Stops playing the sequence.}
59+ procedure stop; message 'stop';
60+ {This property is the length of the currently loaded file in seconds.}
61+ function duration: NSTimeInterval; message 'duration';
62+ {A Boolean value that indicates whether the sequence is playing.
63+ Note: The player may have reached the end of all the events in any of its tracks,
64+ but it will return YES until it is stopped.}
65+ function isPlaying: ObjCBOOL; message 'isPlaying';
66+ {This property’s default value of 1.0 provides normal playback rate. Rate must be > 0.0.}
67+ procedure setRate(newValue: single); message 'setRate:';
68+ function rate: single; message 'rate';
69+ {The current playback position in seconds. No range-checking is done to ensure
70+ currenPosityion is <= Duration.
71+ You can set the currentPosition of the player while the player is playing,
72+ in which case playback will resume at the new position.}
73+ procedure setCurrentPosition(newValue: NSTimeInterval); message 'setCurrentPosition:';
74+ function currentPosition: NSTimeInterval; message 'currentPosition';
75+ end;
76+
77+ { TForm1 }
78+
79+ TForm1 = class(TForm)
80+ DurationLabel: TLabel;
81+ ProgressBar: TProgressBar;
82+ TrackBarLabel: TLabel;
83+ RateLabel: TLabel;
84+ SecondsLabel: TLabel;
85+ PauseButton: TButton;
86+ StopPlayButton: TButton;
87+ PlayMidiButton: TButton;
88+ ElapsedTimer: TTimer;
89+ RateTrackBar: TTrackBar;
90+ procedure PauseButtonClick(Sender: TObject);
91+ procedure PlayMidiButtonClick(Sender: TObject);
92+ procedure RateTrackBarClick(Sender: TObject);
93+ procedure StopPlayButtonClick(Sender: TObject);
94+ procedure ElapsedTimerTimer(Sender: TObject);
95+ procedure RateTrackBarChange(Sender: TObject);
96+ private
97+
98+ public
99+
100+ end;
101+
102+var
103+ Form1: TForm1;
104+ myMidiPlayer : AVMidiPlayer = Nil;
105+ filePos : NSTimeInterval = 0;
106+ fileDuration : NSTimeInterval = 0;
107+ myRate : Single = 1;
108+
109+implementation
110+
111+{$R *.lfm}
112+
113+// Executed when the file finishes playing
114+// or player is paused or player is stopped
115+procedure myCompletionHandler;
116+begin
117+ myMidiPlayer.Stop;
118+ myMidiPlayer.setCurrentPosition(0);
119+
120+ // Button states need to be set here when file
121+ // finishes playing or is paused or is stopped
122+ Form1.PlayMidiButton.Enabled := True;
123+ Form1.StopPlayButton.Enabled := False;
124+ Form1.PauseButton.Enabled := False;
125+
126+ // If file finished playing or has been stopped
127+ // but not paused
128+ if(filePos = 0) then
129+ begin
130+ Form1.SecondsLabel.Caption := '0 of ' + FormatFloat('#', fileDuration) + ' seconds';
131+ Form1.ProgressBar.Position := 0;
132+ myMidiPlayer.release; // recycle
133+ myMidiPlayer := Nil; // memory
134+ end;
135+
136+ {$IFDEF DEBUG}
137+ NSLog(NSStr('Completion handler called'));
138+ {$ENDIF}
139+end;
140+
141+// Play midi procedure
142+procedure PlayMidi(midiFileName : NSString);
143+var
144+ path: NSString;
145+ url : NSURL;
146+ err : NSError;
147+begin
148+ // Do nothing if already playing a midi file
149+ if(myMidiPlayer.IsPlaying) then
150+ exit;
151+
152+ // If player has not been paused
153+ if(filePOS = 0) then
154+ begin
155+ // Path to your application bundle's resource directory
156+ // with the midi filename appended
157+ path := NSBundle.mainBundle.resourcePath.stringByAppendingPathComponent(midiFileName);
158+ url := NSURL.fileURLWithPath(path);
159+
160+ // Create MidiPlayer and load midi file
161+ myMidiPlayer := AVMidiPlayer.alloc.initWithContentsOfURL_error(url, Nil, @err);
162+
163+ // Save file duration
164+ fileDuration := myMidiPlayer.duration;
165+
166+ if Assigned(myMidiPlayer) then
167+ begin
168+ myMidiPlayer.setRate(myRate);
169+ myMidiPlayer.prepareToPlay;
170+ Form1.SecondsLabel.Caption := '0 of ' + FormatFloat('#', fileDuration)
171+ + ' seconds';
172+ Form1.ProgressBar.Max:= Trunc(fileDuration);
173+ myMidiPlayer.play(@myCompletionHandler);
174+ end
175+ else
176+ // Use the Applications > Utilities > Console application to find error messages
177+ NSLog(NSStr('Error in procedure PlayMidi(): %@'), err);
178+ end
179+ // Otherwise resume playing existing file
180+ else
181+ begin
182+ myMidiPlayer.setCurrentPosition(filePos);
183+ Form1.SecondsLabel.Caption := FormatFloat('#', filePos) + ' of '
184+ + FormatFloat('#', fileDuration) + ' seconds';
185+ myMidiPlayer.play(@myCompletionHandler);
186+ end;
187+end;
188+
189+// Play file
190+procedure TForm1.PlayMidiButtonClick(Sender: TObject);
191+begin
192+ PlayMidi(NSStr('Elvis-HoundDog.mid'));
193+ //PlayMidi(NSStr('whitewed.mid'));
194+
195+ // Enable Timer for elapsed time
196+ ElapsedTimer.Enabled := True;
197+
198+ // Set button states
199+ PlayMidiButton.Enabled := False;
200+ PauseButton.Enabled := True;
201+ StopPlayButton.Enabled := True;
202+end;
203+
204+// Pause playback of file
205+procedure TForm1.PauseButtonClick(Sender: TObject);
206+begin
207+ // If file is playing
208+ if(myMidiPlayer.IsPlaying) then
209+ begin
210+ // Stop play (also calls myCompletionHandler)
211+ myMidiPlayer.Stop;
212+
213+ // Save file position for resumption
214+ filePos := myMidiPlayer.currentPosition;
215+
216+ // Disable elapsed timer
217+ ElapsedTimer.Enabled := False;
218+ end;
219+end;
220+
221+// Stop playback of file
222+procedure TForm1.StopPlayButtonClick(Sender: TObject);
223+begin
224+ // If file is playing
225+ if(myMidiPlayer.IsPlaying) then
226+ begin
227+ // Stop play (also calls myCompletionHandler)
228+ myMidiPlayer.stop;
229+
230+ // Zero file position (indicates stopped, not paused)
231+ filePos := 0;
232+ end;
233+end;
234+
235+// Update elaspsed seconds
236+procedure TForm1.ElapsedTimerTimer(Sender: TObject);
237+begin
238+ // Stop timer if file not playing
239+ if(myMidiPlayer.isPlaying = False) then
240+ begin
241+ ElapsedTimer.Enabled := False;
242+ // Workaround to update the button status and labels
243+ // from myCompletionHandler() - otherwise not updated
244+ // in real time (up to 30 seconds later!)
245+ Form1.RateTrackBarClick(RateTrackBar);
246+ end
247+ // Otherwise update elapsed seconds
248+ // and progeess bar position
249+ else
250+ begin
251+ SecondsLabel.Caption := FormatFloat('#', myMidiPlayer.currentPosition)
252+ + ' of ' + FormatFloat('#', fileDuration)
253+ + ' seconds';
254+ ProgressBar.Position := Trunc(myMidiPlayer.currentPosition);
255+ end;
256+end;
257+
258+// Workaround to update the button status and labels
259+// from myCompletionHandler() - otherwise not updated
260+// in real time (up to 30 seconds later!)
261+procedure TForm1.RateTrackBarClick(Sender: TObject);
262+begin
263+ // See Bug: https://bugs.freepascal.org/view.php?id=37125
264+ // RateTrackBar.SetFocus; causes 1 unfreed memory block of 32 bytes on exit
265+ RateTrackBar.Update;
266+ ProgressBar.Update;
267+end;
268+
269+// Adjust playing rate of file
270+procedure TForm1.RateTrackBarChange(Sender: TObject);
271+begin
272+ If(RateTrackBar.Position = 1) then
273+ myRate := 0.50
274+ else if (RateTrackBar.Position = 2) then
275+ myRate := 0.60
276+ else if (RateTrackBar.Position = 3) then
277+ myRate := 0.70
278+ else if (RateTrackBar.Position = 4) then
279+ myRate := 0.80
280+ else if (RateTrackBar.Position = 5) then
281+ myRate := 0.90
282+ else if (RateTrackBar.Position = 6) then
283+ myRate := 1.00
284+ else if (RateTrackBar.Position = 7) then
285+ myRate := 1.10
286+ else if (RateTrackBar.Position = 8) then
287+ myRate := 1.20
288+ else if (RateTrackBar.Position = 9) then
289+ myRate := 1.30
290+ else if (RateTrackBar.Position = 10) then
291+ myRate := 1.40
292+ else
293+ myRate := 1.50;
294+
295+ myMidiPlayer.setRate(myRate);
296+ RateLabel.Caption := FormatFloat('##.##',myRate) + 'x';
297+end;
298+
299+end.
300+```
301+
302+Full source code is available from [SourceForge](https://sourceforge.net/p/lazarus-wiki-projects/code/ci/master/tree/laz_avmidiplayer/).
303+
304+## See also
305+
306+- [[macOS Play Alert Sound|System Sound Services]]
307+- [[macOS Audio Player]]
308+- [[macOS Audio Recorder]]
309+- [[macOS NSSound]]
310+- [[macOS Sound Utilities]]
311+- [[Multimedia Programming]]
312+
313+## External links
314+
315+- [Apple: AVFoundation Framework](https://developer.apple.com/documentation/avfoundation)
316+- [Apple: AVMIDIPlayer](https://developer.apple.com/documentation/avfoundation/avmidiplayer)
317+
318+---
319+*From the Free Pascal wiki, page "macOS MIDI Player", as it stood on 2021-11-08 after a change by Trev, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*
320+Category: Audio
321+Category: Code Snippets
322+Category: Multimedia
323+Category: macOS

Back to history