src/music/qtmidi.cpp
branchcustombridgeheads
changeset 5649 55c8267c933f
parent 5643 3778051e8095
child 6268 4b5241e5dd10
equal deleted inserted replaced
5648:1608018c5ff2 5649:55c8267c933f
       
     1 /* $Id$ */
       
     2 
       
     3 /**
       
     4  * @file qtmidi.c
       
     5  * @brief MIDI music player for MacOS X using QuickTime.
       
     6  *
       
     7  * This music player should work in all MacOS X releases starting from 10.0,
       
     8  * as QuickTime is an integral part of the system since the old days of the
       
     9  * Motorola 68k-based Macintoshes. The only extra dependency apart from
       
    10  * QuickTime itself is Carbon, which is included since 10.0 as well.
       
    11  *
       
    12  * QuickTime gets fooled with the MIDI files from Transport Tycoon Deluxe
       
    13  * because of the @c .gm suffix. To force QuickTime to load the MIDI files
       
    14  * without the need of dealing with the individual QuickTime components
       
    15  * needed to play music (data source, MIDI parser, note allocators,
       
    16  * synthesizers and the like) some Carbon functions are used to set the file
       
    17  * type as seen by QuickTime, using @c FSpSetFInfo() (which modifies the
       
    18  * file's resource fork).
       
    19  */
       
    20 
       
    21 
       
    22 /*
       
    23  * OpenTTD includes.
       
    24  */
       
    25 #define  WindowClass OSX_WindowClass
       
    26 #include <QuickTime/QuickTime.h>
       
    27 #undef   WindowClass
       
    28 
       
    29 #include "../stdafx.h"
       
    30 #include "../openttd.h"
       
    31 #include "qtmidi.h"
       
    32 
       
    33 /*
       
    34  * System includes. We need to workaround with some defines because there's
       
    35  * stuff already defined in QuickTime headers.
       
    36  */
       
    37 #define  OTTD_Random OSX_OTTD_Random
       
    38 #undef   OTTD_Random
       
    39 #undef   WindowClass
       
    40 #undef   SL_ERROR
       
    41 #undef   bool
       
    42 
       
    43 #include <assert.h>
       
    44 #include <unistd.h>
       
    45 #include <fcntl.h>
       
    46 
       
    47 // we need to include debug.h after CoreServices because defining DEBUG will break CoreServices in OSX 10.2
       
    48 #include "../debug.h"
       
    49 
       
    50 
       
    51 enum {
       
    52 	midiType = 'Midi' /**< OSType code for MIDI songs. */
       
    53 };
       
    54 
       
    55 
       
    56 /**
       
    57  * Converts a Unix-like pathname to a @c FSSpec structure which may be
       
    58  * used with functions from several MacOS X frameworks (Carbon, QuickTime,
       
    59  * etc). The pointed file or directory must exist.
       
    60  *
       
    61  * @param *path A string containing a Unix-like path.
       
    62  * @param *spec Pointer to a @c FSSpec structure where the result will be
       
    63  *              stored.
       
    64  * @return Wether the conversion was successful.
       
    65  */
       
    66 static bool PathToFSSpec(const char *path, FSSpec *spec)
       
    67 {
       
    68 	FSRef ref;
       
    69 	assert(spec != NULL);
       
    70 	assert(path != NULL);
       
    71 
       
    72 	return
       
    73 		FSPathMakeRef((UInt8*)path, &ref, NULL) == noErr &&
       
    74 		FSGetCatalogInfo(&ref, kFSCatInfoNone, NULL, NULL, spec, NULL) == noErr;
       
    75 }
       
    76 
       
    77 
       
    78 /**
       
    79  * Sets the @c OSType of a given file to @c 'Midi', but only if it's not
       
    80  * already set.
       
    81  *
       
    82  * @param *spec A @c FSSpec structure referencing a file.
       
    83  */
       
    84 static void SetMIDITypeIfNeeded(const FSSpec *spec)
       
    85 {
       
    86 	FSRef ref;
       
    87 	FSCatalogInfo catalogInfo;
       
    88 
       
    89 	assert(spec);
       
    90 
       
    91 	if (noErr != FSpMakeFSRef(spec, &ref)) return;
       
    92 	if (noErr != FSGetCatalogInfo(&ref, kFSCatInfoNodeFlags | kFSCatInfoFinderInfo, &catalogInfo, NULL, NULL, NULL)) return;
       
    93 	if (!(catalogInfo.nodeFlags & kFSNodeIsDirectoryMask)) {
       
    94 		FileInfo * const info = (FileInfo *) catalogInfo.finderInfo;
       
    95 		if (info->fileType != midiType && !(info->finderFlags & kIsAlias)) {
       
    96 			OSErr e;
       
    97 			info->fileType = midiType;
       
    98 			e = FSSetCatalogInfo(&ref, kFSCatInfoFinderInfo, &catalogInfo);
       
    99 			if (e == noErr) {
       
   100 				DEBUG(driver, 3, "qtmidi: changed filetype to 'Midi'");
       
   101 			} else {
       
   102 				DEBUG(driver, 0, "qtmidi: changing filetype to 'Midi' failed - error %d", e);
       
   103 			}
       
   104 		}
       
   105 	}
       
   106 }
       
   107 
       
   108 
       
   109 /**
       
   110  * Loads a MIDI file and returns it as a QuickTime Movie structure.
       
   111  *
       
   112  * @param *path String with the path of an existing MIDI file.
       
   113  * @param *moov Pointer to a @c Movie where the result will be stored.
       
   114  * @return Wether the file was loaded and the @c Movie successfully created.
       
   115  */
       
   116 static bool LoadMovieForMIDIFile(const char *path, Movie *moov)
       
   117 {
       
   118 	int fd;
       
   119 	int ret;
       
   120 	char magic[4];
       
   121 	FSSpec fsspec;
       
   122 	short refnum = 0;
       
   123 	short resid  = 0;
       
   124 
       
   125 	assert(path != NULL);
       
   126 	assert(moov != NULL);
       
   127 
       
   128 	DEBUG(driver, 2, "qtmidi: start loading '%s'...", path);
       
   129 
       
   130 	/*
       
   131 	 * XXX Manual check for MIDI header ('MThd'), as I don't know how to make
       
   132 	 * QuickTime load MIDI files without a .mid suffix without knowing it's
       
   133 	 * a MIDI file and setting the OSType of the file to the 'Midi' value.
       
   134 	 * Perhahaps ugly, but it seems that it does the Right Thing(tm).
       
   135 	 */
       
   136 	fd = open(path, O_RDONLY, 0);
       
   137 	if (fd == -1) return false;
       
   138 	ret = read(fd, magic, 4);
       
   139 	close(fd);
       
   140 	if (ret < 4) return false;
       
   141 
       
   142 	DEBUG(driver, 3, "qtmidi: header is '%.4s'", magic);
       
   143 	if (magic[0] != 'M' || magic[1] != 'T' || magic[2] != 'h' || magic[3] != 'd')
       
   144 		return false;
       
   145 
       
   146 	if (!PathToFSSpec(path, &fsspec)) return false;
       
   147 	SetMIDITypeIfNeeded(&fsspec);
       
   148 
       
   149 	if (OpenMovieFile(&fsspec, &refnum, fsRdPerm) != noErr) return false;
       
   150 	DEBUG(driver, 3, "qtmidi: '%s' successfully opened", path);
       
   151 
       
   152 	if (noErr != NewMovieFromFile(moov, refnum, &resid, NULL,
       
   153 				newMovieActive | newMovieDontAskUnresolvedDataRefs, NULL)) {
       
   154 		CloseMovieFile(refnum);
       
   155 		return false;
       
   156 	}
       
   157 	DEBUG(driver, 3, "qtmidi: movie container created");
       
   158 
       
   159 	CloseMovieFile(refnum);
       
   160 	return true;
       
   161 }
       
   162 
       
   163 
       
   164 /**
       
   165  * Flag which has the @c true value when QuickTime is available and
       
   166  * initialized.
       
   167  */
       
   168 static bool _quicktime_started = false;
       
   169 
       
   170 
       
   171 /**
       
   172  * Initialize QuickTime if needed. This function sets the
       
   173  * #_quicktime_started flag to @c true if QuickTime is present in the system
       
   174  * and it was initialized properly.
       
   175  */
       
   176 static void InitQuickTimeIfNeeded(void)
       
   177 {
       
   178 	OSStatus dummy;
       
   179 
       
   180 	if (_quicktime_started) return;
       
   181 
       
   182 	DEBUG(driver, 2, "qtmidi: initializing Quicktime");
       
   183 	/* Be polite: check wether QuickTime is available and initialize it. */
       
   184 	_quicktime_started =
       
   185 		(noErr == Gestalt(gestaltQuickTime, &dummy)) &&
       
   186 		(noErr == EnterMovies());
       
   187 	if (!_quicktime_started) DEBUG(driver, 0, "qtmidi: Quicktime initialization failed!");
       
   188 }
       
   189 
       
   190 
       
   191 /** Possible states of the QuickTime music driver. */
       
   192 enum {
       
   193 	QT_STATE_IDLE, /**< No file loaded. */
       
   194 	QT_STATE_PLAY, /**< File loaded, playing. */
       
   195 	QT_STATE_STOP, /**< File loaded, stopped. */
       
   196 };
       
   197 
       
   198 
       
   199 static Movie _quicktime_movie;                  /**< Current QuickTime @c Movie. */
       
   200 static byte  _quicktime_volume = 127;           /**< Current volume. */
       
   201 static int   _quicktime_state  = QT_STATE_IDLE; /**< Current player state. */
       
   202 
       
   203 
       
   204 /**
       
   205  * Maps OpenTTD volume to QuickTime notion of volume.
       
   206  */
       
   207 #define VOLUME  ((short)((0x00FF & _quicktime_volume) << 1))
       
   208 
       
   209 
       
   210 static void StopSong(void);
       
   211 
       
   212 
       
   213 /**
       
   214  * Initialized the MIDI player, including QuickTime initialization.
       
   215  *
       
   216  * @todo Give better error messages by inspecting error codes returned by
       
   217  * @c Gestalt() and @c EnterMovies(). Needs changes in
       
   218  * #InitQuickTimeIfNeeded.
       
   219  */
       
   220 static const char* StartDriver(const char * const *parm)
       
   221 {
       
   222 	InitQuickTimeIfNeeded();
       
   223 	return (_quicktime_started) ? NULL : "can't initialize QuickTime";
       
   224 }
       
   225 
       
   226 
       
   227 /**
       
   228  * Checks wether the player is active.
       
   229  *
       
   230  * This function is called at regular intervals from OpenTTD's main loop, so
       
   231  * we call @c MoviesTask() from here to let QuickTime do its work.
       
   232  */
       
   233 static bool SongIsPlaying(void)
       
   234 {
       
   235 	if (!_quicktime_started) return true;
       
   236 
       
   237 	switch (_quicktime_state) {
       
   238 		case QT_STATE_IDLE:
       
   239 		case QT_STATE_STOP:
       
   240 			/* Do nothing. */
       
   241 			break;
       
   242 		case QT_STATE_PLAY:
       
   243 			MoviesTask(_quicktime_movie, 0);
       
   244 			/* Check wether movie ended. */
       
   245 			if (IsMovieDone(_quicktime_movie) ||
       
   246 					(GetMovieTime(_quicktime_movie, NULL) >=
       
   247 					 GetMovieDuration(_quicktime_movie)))
       
   248 				_quicktime_state = QT_STATE_STOP;
       
   249 	}
       
   250 
       
   251 	return _quicktime_state == QT_STATE_PLAY;
       
   252 }
       
   253 
       
   254 
       
   255 /**
       
   256  * Stops the MIDI player.
       
   257  *
       
   258  * Stops playing and frees any used resources before returning. As it
       
   259  * deinitilizes QuickTime, the #_quicktime_started flag is set to @c false.
       
   260  */
       
   261 static void StopDriver(void)
       
   262 {
       
   263 	if (!_quicktime_started) return;
       
   264 
       
   265 	DEBUG(driver, 2, "qtmidi: stopping driver...");
       
   266 	switch (_quicktime_state) {
       
   267 		case QT_STATE_IDLE:
       
   268 			DEBUG(driver, 3, "qtmidi: stopping not needed, already idle");
       
   269 			/* Do nothing. */
       
   270 			break;
       
   271 		case QT_STATE_PLAY:
       
   272 			StopSong();
       
   273 		case QT_STATE_STOP:
       
   274 			DisposeMovie(_quicktime_movie);
       
   275 	}
       
   276 
       
   277 	ExitMovies();
       
   278 	_quicktime_started = false;
       
   279 }
       
   280 
       
   281 
       
   282 /**
       
   283  * Starts playing a new song.
       
   284  *
       
   285  * @param filename Path to a MIDI file.
       
   286  */
       
   287 static void PlaySong(const char *filename)
       
   288 {
       
   289 	if (!_quicktime_started) return;
       
   290 
       
   291 	DEBUG(driver, 2, "qtmidi: trying to play '%s'", filename);
       
   292 	switch (_quicktime_state) {
       
   293 		case QT_STATE_PLAY:
       
   294 			StopSong();
       
   295 			DEBUG(driver, 3, "qtmidi: previous tune stopped");
       
   296 			/* XXX Fall-through -- no break needed. */
       
   297 		case QT_STATE_STOP:
       
   298 			DisposeMovie(_quicktime_movie);
       
   299 			DEBUG(driver, 3, "qtmidi: previous tune disposed");
       
   300 			_quicktime_state = QT_STATE_IDLE;
       
   301 			/* XXX Fall-through -- no break needed. */
       
   302 		case QT_STATE_IDLE:
       
   303 			LoadMovieForMIDIFile(filename, &_quicktime_movie);
       
   304 			SetMovieVolume(_quicktime_movie, VOLUME);
       
   305 			StartMovie(_quicktime_movie);
       
   306 			_quicktime_state = QT_STATE_PLAY;
       
   307 	}
       
   308 	DEBUG(driver, 3, "qtmidi: playing '%s'", filename);
       
   309 }
       
   310 
       
   311 
       
   312 /**
       
   313  * Stops playing the current song, if the player is active.
       
   314  */
       
   315 static void StopSong(void)
       
   316 {
       
   317 	if (!_quicktime_started) return;
       
   318 
       
   319 	switch (_quicktime_state) {
       
   320 		case QT_STATE_IDLE:
       
   321 			/* XXX Fall-through -- no break needed. */
       
   322 		case QT_STATE_STOP:
       
   323 			DEBUG(driver, 3, "qtmidi: stop requested, but already idle");
       
   324 			/* Do nothing. */
       
   325 			break;
       
   326 		case QT_STATE_PLAY:
       
   327 			StopMovie(_quicktime_movie);
       
   328 			_quicktime_state = QT_STATE_STOP;
       
   329 			DEBUG(driver, 3, "qtmidi: player stopped");
       
   330 	}
       
   331 }
       
   332 
       
   333 
       
   334 /**
       
   335  * Changes the playing volume of the MIDI player.
       
   336  *
       
   337  * As QuickTime controls volume in a per-movie basis, the desired volume is
       
   338  * stored in #_quicktime_volume, and the volume is set here using the
       
   339  * #VOLUME macro, @b and when loading new song in #PlaySong.
       
   340  *
       
   341  * @param vol The desired volume, range of the value is @c 0-127
       
   342  */
       
   343 static void SetVolume(byte vol)
       
   344 {
       
   345 	if (!_quicktime_started) return;
       
   346 
       
   347 	_quicktime_volume = vol;
       
   348 
       
   349 	DEBUG(driver, 2, "qtmidi: set volume to %u (%hi)", vol, VOLUME);
       
   350 	switch (_quicktime_state) {
       
   351 		case QT_STATE_IDLE:
       
   352 			/* Do nothing. */
       
   353 			break;
       
   354 		case QT_STATE_PLAY:
       
   355 		case QT_STATE_STOP:
       
   356 			SetMovieVolume(_quicktime_movie, VOLUME);
       
   357 	}
       
   358 }
       
   359 
       
   360 
       
   361 /**
       
   362  * Table of callbacks that implement the QuickTime MIDI player.
       
   363  */
       
   364 const HalMusicDriver _qtime_music_driver = {
       
   365 	StartDriver,
       
   366 	StopDriver,
       
   367 	PlaySong,
       
   368 	StopSong,
       
   369 	SongIsPlaying,
       
   370 	SetVolume,
       
   371 };