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
| Old | New | Line | |
|---|---|---|---|
| 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 | + |  | |
| 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 |
