tron@3970: /* $Id$ */ bjarni@2496: bjarni@2496: /** belugas@6443: * @file qtmidi.cpp 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: egladil@8461: #define MAC_OS_X_VERSION_MIN_REQUIRED MAC_OS_X_VERSION_10_3 egladil@8461: #include egladil@8461: 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 "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: peter1138@7666: static FMusicDriver_QtMidi iFMusicDriver_QtMidi; peter1138@7666: bjarni@2496: bjarni@2496: enum { bjarni@2496: midiType = 'Midi' /**< OSType code for MIDI songs. */ 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: */ egladil@8335: static void SetMIDITypeIfNeeded(const FSRef *ref) bjarni@2496: { bjarni@5684: FSCatalogInfo catalogInfo; bjarni@5684: egladil@8335: assert(ref); bjarni@2496: egladil@8335: if (noErr != FSGetCatalogInfo(ref, kFSCatInfoNodeFlags | kFSCatInfoFinderInfo, &catalogInfo, NULL, NULL, NULL)) return; bjarni@5684: if (!(catalogInfo.nodeFlags & kFSNodeIsDirectoryMask)) { bjarni@5684: FileInfo * const info = (FileInfo *) catalogInfo.finderInfo; bjarni@5684: if (info->fileType != midiType && !(info->finderFlags & kIsAlias)) { bjarni@5684: OSErr e; bjarni@5684: info->fileType = midiType; egladil@8335: e = FSSetCatalogInfo(ref, kFSCatInfoFinderInfo, &catalogInfo); bjarni@5684: if (e == noErr) { bjarni@5684: DEBUG(driver, 3, "qtmidi: changed filetype to 'Midi'"); bjarni@5684: } else { bjarni@5684: DEBUG(driver, 0, "qtmidi: changing filetype to 'Midi' failed - error %d", e); bjarni@5684: } bjarni@5684: } 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]; egladil@8335: FSRef fsref; 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: egladil@8335: if (noErr != FSPathMakeRef((const UInt8 *) path, &fsref, NULL)) return false; egladil@8335: SetMIDITypeIfNeeded(&fsref); bjarni@2496: egladil@8335: if (noErr != FSGetCatalogInfo(&fsref, kFSCatInfoNone, NULL, NULL, &fsspec, NULL)) return false; 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: */ rubidium@6573: static void InitQuickTimeIfNeeded() 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: /** 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: */ peter1138@7666: const char *MusicDriver_QtMidi::Start(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: */ peter1138@7666: bool MusicDriver_QtMidi::IsSongPlaying() 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: */ peter1138@7666: void MusicDriver_QtMidi::Stop() 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: */ peter1138@7666: void MusicDriver_QtMidi::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: */ peter1138@7666: void MusicDriver_QtMidi::StopSong() 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: */ peter1138@7666: void MusicDriver_QtMidi::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: