tron@3970: /* $Id$ */ bjarni@2496: bjarni@2496: /** bjarni@2496: * @file qtmidi.c bjarni@2496: * @brief MIDI music player for MacOS X using QuickTime. bjarni@2496: * bjarni@2496: * This music player should work in all MacOS X releases starting from 10.0, bjarni@2496: * as QuickTime is an integral part of the system since the old days of the bjarni@2496: * Motorola 68k-based Macintoshes. The only extra dependency apart from bjarni@2496: * QuickTime itself is Carbon, which is included since 10.0 as well. bjarni@2496: * bjarni@2496: * QuickTime gets fooled with the MIDI files from Transport Tycoon Deluxe bjarni@2496: * because of the @c .gm suffix. To force QuickTime to load the MIDI files bjarni@2496: * without the need of dealing with the individual QuickTime components bjarni@2496: * needed to play music (data source, MIDI parser, note allocators, bjarni@2496: * synthesizers and the like) some Carbon functions are used to set the file bjarni@2496: * type as seen by QuickTime, using @c FSpSetFInfo() (which modifies the bjarni@2496: * file's resource fork). bjarni@2496: */ bjarni@2496: bjarni@2496: bjarni@2496: /* bjarni@2496: * OpenTTD includes. bjarni@2496: */ bjarni@3016: #define WindowClass OSX_WindowClass bjarni@3016: #include bjarni@3016: #undef WindowClass bjarni@3016: bjarni@2496: #include "../stdafx.h" bjarni@2496: #include "../openttd.h" bjarni@2496: #include "qtmidi.h" bjarni@2496: bjarni@2496: /* bjarni@2496: * System includes. We need to workaround with some defines because there's bjarni@2496: * stuff already defined in QuickTime headers. bjarni@2496: */ bjarni@2496: #define OTTD_Random OSX_OTTD_Random bjarni@2496: #undef OTTD_Random bjarni@2496: #undef WindowClass bjarni@2496: #undef SL_ERROR bjarni@2496: #undef bool bjarni@2496: bjarni@2496: #include bjarni@2496: #include bjarni@2496: #include bjarni@2496: bjarni@2741: // we need to include debug.h after CoreServices because defining DEBUG will break CoreServices in OSX 10.2 bjarni@2741: #include "../debug.h" bjarni@2496: bjarni@2496: bjarni@2496: enum { bjarni@2496: midiType = 'Midi' /**< OSType code for MIDI songs. */ bjarni@2496: }; bjarni@2496: bjarni@2496: bjarni@2496: /** bjarni@2496: * Converts a Unix-like pathname to a @c FSSpec structure which may be bjarni@2496: * used with functions from several MacOS X frameworks (Carbon, QuickTime, bjarni@2496: * etc). The pointed file or directory must exist. bjarni@2496: * bjarni@2496: * @param *path A string containing a Unix-like path. bjarni@2496: * @param *spec Pointer to a @c FSSpec structure where the result will be bjarni@2496: * stored. bjarni@2496: * @return Wether the conversion was successful. bjarni@2496: */ bjarni@2496: static bool PathToFSSpec(const char *path, FSSpec *spec) bjarni@2496: { bjarni@2496: FSRef ref; tron@4000: assert(spec != NULL); tron@4000: assert(path != NULL); bjarni@2496: tron@4000: return tron@4000: FSPathMakeRef((UInt8*)path, &ref, NULL) == noErr && tron@4000: FSGetCatalogInfo(&ref, kFSCatInfoNone, NULL, NULL, spec, NULL) == noErr; bjarni@2496: } bjarni@2496: bjarni@2496: bjarni@2496: /** bjarni@2496: * Sets the @c OSType of a given file to @c 'Midi', but only if it's not bjarni@2496: * already set. bjarni@2496: * bjarni@2496: * @param *spec A @c FSSpec structure referencing a file. bjarni@2496: */ bjarni@2496: static void SetMIDITypeIfNeeded(const FSSpec *spec) bjarni@2496: { celestar@5623: FSRef ref; celestar@5623: FSCatalogInfo catalogInfo; celestar@5623: bjarni@2496: assert(spec); bjarni@2496: celestar@5623: if (noErr != FSpMakeFSRef(spec, &ref)) return; celestar@5623: if (noErr != FSGetCatalogInfo(&ref, kFSCatInfoNodeFlags | kFSCatInfoFinderInfo, &catalogInfo, NULL, NULL, NULL)) return; celestar@5623: if (!(catalogInfo.nodeFlags & kFSNodeIsDirectoryMask)) { celestar@5623: FileInfo * const info = (FileInfo *) catalogInfo.finderInfo; celestar@5623: if (info->fileType != midiType && !(info->finderFlags & kIsAlias)) { celestar@5623: OSErr e; celestar@5623: info->fileType = midiType; celestar@5623: e = FSSetCatalogInfo(&ref, kFSCatInfoFinderInfo, &catalogInfo); celestar@5623: if (e == noErr) { celestar@5623: DEBUG(driver, 3, "qtmidi: changed filetype to 'Midi'"); celestar@5623: } else { celestar@5623: DEBUG(driver, 0, "qtmidi: changing filetype to 'Midi' failed - error %d", e); celestar@5623: } celestar@5623: } bjarni@2496: } bjarni@2496: } bjarni@2496: bjarni@2496: bjarni@2496: /** bjarni@2496: * Loads a MIDI file and returns it as a QuickTime Movie structure. bjarni@2496: * bjarni@2496: * @param *path String with the path of an existing MIDI file. bjarni@2496: * @param *moov Pointer to a @c Movie where the result will be stored. bjarni@2496: * @return Wether the file was loaded and the @c Movie successfully created. bjarni@2496: */ bjarni@2496: static bool LoadMovieForMIDIFile(const char *path, Movie *moov) bjarni@2496: { bjarni@2496: int fd; bjarni@2496: int ret; bjarni@2496: char magic[4]; bjarni@2496: FSSpec fsspec; bjarni@2496: short refnum = 0; bjarni@2496: short resid = 0; bjarni@2496: tron@4000: assert(path != NULL); tron@4000: assert(moov != NULL); bjarni@2496: Darkvater@5576: DEBUG(driver, 2, "qtmidi: start loading '%s'...", path); bjarni@2496: bjarni@2496: /* bjarni@2496: * XXX Manual check for MIDI header ('MThd'), as I don't know how to make bjarni@2496: * QuickTime load MIDI files without a .mid suffix without knowing it's bjarni@2496: * a MIDI file and setting the OSType of the file to the 'Midi' value. bjarni@2496: * Perhahaps ugly, but it seems that it does the Right Thing(tm). bjarni@2496: */ tron@4000: fd = open(path, O_RDONLY, 0); tron@4000: if (fd == -1) return false; bjarni@2496: ret = read(fd, magic, 4); bjarni@2496: close(fd); bjarni@2496: if (ret < 4) return false; bjarni@2496: Darkvater@5576: DEBUG(driver, 3, "qtmidi: header is '%.4s'", magic); bjarni@2496: if (magic[0] != 'M' || magic[1] != 'T' || magic[2] != 'h' || magic[3] != 'd') bjarni@2496: return false; bjarni@2496: tron@4000: if (!PathToFSSpec(path, &fsspec)) return false; bjarni@2496: SetMIDITypeIfNeeded(&fsspec); bjarni@2496: tron@4000: if (OpenMovieFile(&fsspec, &refnum, fsRdPerm) != noErr) return false; Darkvater@5576: DEBUG(driver, 3, "qtmidi: '%s' successfully opened", path); bjarni@2496: bjarni@2496: if (noErr != NewMovieFromFile(moov, refnum, &resid, NULL, Darkvater@5576: newMovieActive | newMovieDontAskUnresolvedDataRefs, NULL)) { bjarni@2496: CloseMovieFile(refnum); bjarni@2496: return false; bjarni@2496: } Darkvater@5576: DEBUG(driver, 3, "qtmidi: movie container created"); bjarni@2496: bjarni@2496: CloseMovieFile(refnum); bjarni@2496: return true; bjarni@2496: } bjarni@2496: bjarni@2496: bjarni@2496: /** bjarni@2496: * Flag which has the @c true value when QuickTime is available and bjarni@2496: * initialized. bjarni@2496: */ bjarni@2496: static bool _quicktime_started = false; bjarni@2496: bjarni@2496: bjarni@2496: /** bjarni@2496: * Initialize QuickTime if needed. This function sets the bjarni@2496: * #_quicktime_started flag to @c true if QuickTime is present in the system bjarni@2496: * and it was initialized properly. bjarni@2496: */ bjarni@2496: static void InitQuickTimeIfNeeded(void) bjarni@2496: { bjarni@2496: OSStatus dummy; bjarni@2496: bjarni@2496: if (_quicktime_started) return; bjarni@2496: Darkvater@5576: DEBUG(driver, 2, "qtmidi: initializing Quicktime"); bjarni@2496: /* Be polite: check wether QuickTime is available and initialize it. */ bjarni@2496: _quicktime_started = bjarni@2496: (noErr == Gestalt(gestaltQuickTime, &dummy)) && bjarni@2496: (noErr == EnterMovies()); bjarni@5577: if (!_quicktime_started) DEBUG(driver, 0, "qtmidi: Quicktime initialization failed!"); bjarni@2496: } bjarni@2496: bjarni@2496: bjarni@2496: /** Possible states of the QuickTime music driver. */ tron@4000: enum { bjarni@2496: QT_STATE_IDLE, /**< No file loaded. */ bjarni@2496: QT_STATE_PLAY, /**< File loaded, playing. */ bjarni@2496: QT_STATE_STOP, /**< File loaded, stopped. */ bjarni@2496: }; bjarni@2496: bjarni@2496: bjarni@2496: static Movie _quicktime_movie; /**< Current QuickTime @c Movie. */ bjarni@2496: static byte _quicktime_volume = 127; /**< Current volume. */ bjarni@2496: static int _quicktime_state = QT_STATE_IDLE; /**< Current player state. */ bjarni@2496: bjarni@2496: bjarni@2496: /** bjarni@2496: * Maps OpenTTD volume to QuickTime notion of volume. bjarni@2496: */ bjarni@2496: #define VOLUME ((short)((0x00FF & _quicktime_volume) << 1)) bjarni@2496: bjarni@2496: bjarni@2496: static void StopSong(void); bjarni@2496: bjarni@2496: bjarni@2496: /** bjarni@2496: * Initialized the MIDI player, including QuickTime initialization. bjarni@2496: * bjarni@2496: * @todo Give better error messages by inspecting error codes returned by bjarni@2496: * @c Gestalt() and @c EnterMovies(). Needs changes in bjarni@2496: * #InitQuickTimeIfNeeded. bjarni@2496: */ bjarni@2496: static const char* StartDriver(const char * const *parm) bjarni@2496: { bjarni@2496: InitQuickTimeIfNeeded(); bjarni@2496: return (_quicktime_started) ? NULL : "can't initialize QuickTime"; bjarni@2496: } bjarni@2496: bjarni@2496: bjarni@2496: /** bjarni@2496: * Checks wether the player is active. bjarni@2496: * bjarni@2496: * This function is called at regular intervals from OpenTTD's main loop, so bjarni@2496: * we call @c MoviesTask() from here to let QuickTime do its work. bjarni@2496: */ bjarni@2496: static bool SongIsPlaying(void) bjarni@2496: { bjarni@2496: if (!_quicktime_started) return true; bjarni@2496: bjarni@2496: switch (_quicktime_state) { bjarni@2496: case QT_STATE_IDLE: bjarni@2496: case QT_STATE_STOP: bjarni@2496: /* Do nothing. */ bjarni@2496: break; bjarni@2496: case QT_STATE_PLAY: bjarni@2496: MoviesTask(_quicktime_movie, 0); bjarni@2496: /* Check wether movie ended. */ bjarni@2496: if (IsMovieDone(_quicktime_movie) || bjarni@2496: (GetMovieTime(_quicktime_movie, NULL) >= bjarni@2496: GetMovieDuration(_quicktime_movie))) bjarni@2496: _quicktime_state = QT_STATE_STOP; bjarni@2496: } bjarni@2496: tron@4000: return _quicktime_state == QT_STATE_PLAY; bjarni@2496: } bjarni@2496: bjarni@2496: bjarni@2496: /** bjarni@2496: * Stops the MIDI player. bjarni@2496: * bjarni@2496: * Stops playing and frees any used resources before returning. As it bjarni@2496: * deinitilizes QuickTime, the #_quicktime_started flag is set to @c false. bjarni@2496: */ bjarni@2496: static void StopDriver(void) bjarni@2496: { bjarni@2496: if (!_quicktime_started) return; bjarni@2496: Darkvater@5576: DEBUG(driver, 2, "qtmidi: stopping driver..."); bjarni@2496: switch (_quicktime_state) { bjarni@2496: case QT_STATE_IDLE: Darkvater@5576: DEBUG(driver, 3, "qtmidi: stopping not needed, already idle"); bjarni@2496: /* Do nothing. */ bjarni@2496: break; bjarni@2496: case QT_STATE_PLAY: bjarni@2496: StopSong(); bjarni@2496: case QT_STATE_STOP: bjarni@2496: DisposeMovie(_quicktime_movie); bjarni@2496: } bjarni@2496: bjarni@2496: ExitMovies(); bjarni@2496: _quicktime_started = false; bjarni@2496: } bjarni@2496: bjarni@2496: bjarni@2496: /** bjarni@2496: * Starts playing a new song. bjarni@2496: * bjarni@2496: * @param filename Path to a MIDI file. bjarni@2496: */ bjarni@2496: static void PlaySong(const char *filename) bjarni@2496: { bjarni@2496: if (!_quicktime_started) return; bjarni@2496: Darkvater@5576: DEBUG(driver, 2, "qtmidi: trying to play '%s'", filename); bjarni@2496: switch (_quicktime_state) { bjarni@2496: case QT_STATE_PLAY: bjarni@2496: StopSong(); Darkvater@5576: DEBUG(driver, 3, "qtmidi: previous tune stopped"); bjarni@2496: /* XXX Fall-through -- no break needed. */ bjarni@2496: case QT_STATE_STOP: bjarni@2496: DisposeMovie(_quicktime_movie); Darkvater@5576: DEBUG(driver, 3, "qtmidi: previous tune disposed"); bjarni@2496: _quicktime_state = QT_STATE_IDLE; bjarni@2496: /* XXX Fall-through -- no break needed. */ bjarni@2496: case QT_STATE_IDLE: bjarni@2496: LoadMovieForMIDIFile(filename, &_quicktime_movie); bjarni@2496: SetMovieVolume(_quicktime_movie, VOLUME); bjarni@2496: StartMovie(_quicktime_movie); bjarni@2496: _quicktime_state = QT_STATE_PLAY; bjarni@2496: } Darkvater@5576: DEBUG(driver, 3, "qtmidi: playing '%s'", filename); bjarni@2496: } bjarni@2496: bjarni@2496: bjarni@2496: /** bjarni@2496: * Stops playing the current song, if the player is active. bjarni@2496: */ bjarni@2496: static void StopSong(void) bjarni@2496: { bjarni@2496: if (!_quicktime_started) return; bjarni@2496: bjarni@2496: switch (_quicktime_state) { bjarni@2496: case QT_STATE_IDLE: bjarni@2496: /* XXX Fall-through -- no break needed. */ bjarni@2496: case QT_STATE_STOP: Darkvater@5576: DEBUG(driver, 3, "qtmidi: stop requested, but already idle"); bjarni@2496: /* Do nothing. */ bjarni@2496: break; bjarni@2496: case QT_STATE_PLAY: bjarni@2496: StopMovie(_quicktime_movie); bjarni@2496: _quicktime_state = QT_STATE_STOP; Darkvater@5576: DEBUG(driver, 3, "qtmidi: player stopped"); bjarni@2496: } bjarni@2496: } bjarni@2496: bjarni@2496: bjarni@2496: /** bjarni@2496: * Changes the playing volume of the MIDI player. bjarni@2496: * bjarni@2496: * As QuickTime controls volume in a per-movie basis, the desired volume is bjarni@2496: * stored in #_quicktime_volume, and the volume is set here using the bjarni@2496: * #VOLUME macro, @b and when loading new song in #PlaySong. bjarni@2496: * bjarni@2496: * @param vol The desired volume, range of the value is @c 0-127 bjarni@2496: */ bjarni@2496: static void SetVolume(byte vol) bjarni@2496: { bjarni@2496: if (!_quicktime_started) return; bjarni@2496: bjarni@2496: _quicktime_volume = vol; bjarni@2496: Darkvater@5576: DEBUG(driver, 2, "qtmidi: set volume to %u (%hi)", vol, VOLUME); bjarni@2496: switch (_quicktime_state) { bjarni@2496: case QT_STATE_IDLE: bjarni@2496: /* Do nothing. */ bjarni@2496: break; bjarni@2496: case QT_STATE_PLAY: bjarni@2496: case QT_STATE_STOP: bjarni@2496: SetMovieVolume(_quicktime_movie, VOLUME); bjarni@2496: } bjarni@2496: } bjarni@2496: bjarni@2496: bjarni@2496: /** bjarni@2496: * Table of callbacks that implement the QuickTime MIDI player. bjarni@2496: */ bjarni@2496: const HalMusicDriver _qtime_music_driver = { bjarni@2496: StartDriver, bjarni@2496: StopDriver, bjarni@2496: PlaySong, bjarni@2496: StopSong, bjarni@2496: SongIsPlaying, bjarni@2496: SetVolume, bjarni@2496: };