diff options
-rw-r--r-- | Makefile | 1 | ||||
-rw-r--r-- | driver.c | 4 | ||||
-rw-r--r-- | music/qtmidi.c | 379 | ||||
-rw-r--r-- | music/qtmidi.h | 15 |
4 files changed, 399 insertions, 0 deletions
@@ -711,6 +711,7 @@ endif ifdef OSX OBJC_SOURCES += os/macosx/macos.m +C_SOURCES += music/qtmidi.c endif OBJS = $(C_SOURCES:%.c=%.o) $(CXX_SOURCES:%.cpp=%.o) $(OBJC_SOURCES:%.m=%.o) @@ -14,6 +14,7 @@ #include "music/null_m.h" #include "music/os2_m.h" #include "music/win32_m.h" +#include "music/qtmidi.h" #include "sound/null_s.h" #include "sound/sdl_s.h" @@ -51,6 +52,9 @@ static const DriverDesc _music_driver_descs[] = { #ifdef WIN32 M("win32", "Win32 MIDI Driver", &_win32_music_driver), #endif +#ifdef __APPLE__ + M("qt", "QuickTime MIDI Driver", &_qtime_music_driver), +#endif #ifdef UNIX #if !defined(__MORPHOS__) && !defined(__AMIGA__) M("extmidi", "External MIDI Driver", &_extmidi_music_driver), diff --git a/music/qtmidi.c b/music/qtmidi.c new file mode 100644 index 000000000..be20f1cb0 --- /dev/null +++ b/music/qtmidi.c @@ -0,0 +1,379 @@ +/* + * qtmidi.c - MIDI Player for OpenTTD using QuickTime (MacOS X). + */ + +/** + * @file qtmidi.c + * @brief MIDI music player for MacOS X using QuickTime. + * + * This music player should work in all MacOS X releases starting from 10.0, + * as QuickTime is an integral part of the system since the old days of the + * Motorola 68k-based Macintoshes. The only extra dependency apart from + * QuickTime itself is Carbon, which is included since 10.0 as well. + * + * QuickTime gets fooled with the MIDI files from Transport Tycoon Deluxe + * because of the @c .gm suffix. To force QuickTime to load the MIDI files + * without the need of dealing with the individual QuickTime components + * needed to play music (data source, MIDI parser, note allocators, + * synthesizers and the like) some Carbon functions are used to set the file + * type as seen by QuickTime, using @c FSpSetFInfo() (which modifies the + * file's resource fork). + */ + + +/* + * OpenTTD includes. + */ +#include "../stdafx.h" +#include "../openttd.h" +#include "../debug.h" +#include "qtmidi.h" + +/* + * System includes. We need to workaround with some defines because there's + * stuff already defined in QuickTime headers. + */ +#define bool OSX_bool +#define Rect OSX_Rect +#define Point OSX_Point +#define SL_ERROR OSX_SL_ERROR +#define WindowClass OSX_WindowClass +#define OTTD_Random OSX_OTTD_Random +#include <CoreServices/CoreServices.h> +#include <QuickTime/QuickTime.h> +#undef OTTD_Random +#undef WindowClass +#undef SL_ERROR +#undef Point +#undef Rect +#undef bool + +#include <assert.h> +#include <unistd.h> +#include <fcntl.h> + + + +enum { + midiType = 'Midi' /**< OSType code for MIDI songs. */ +}; + + +/** + * Converts a Unix-like pathname to a @c FSSpec structure which may be + * used with functions from several MacOS X frameworks (Carbon, QuickTime, + * etc). The pointed file or directory must exist. + * + * @param *path A string containing a Unix-like path. + * @param *spec Pointer to a @c FSSpec structure where the result will be + * stored. + * @return Wether the conversion was successful. + */ +static bool PathToFSSpec(const char *path, FSSpec *spec) +{ + FSRef ref; + assert(spec); + assert(path); + + if (noErr != FSPathMakeRef((UInt8*) path, &ref, NULL)) + return false; + + return (noErr == + FSGetCatalogInfo(&ref, kFSCatInfoNone, NULL, NULL, spec, NULL)); +} + + +/** + * Sets the @c OSType of a given file to @c 'Midi', but only if it's not + * already set. + * + * @param *spec A @c FSSpec structure referencing a file. + */ +static void SetMIDITypeIfNeeded(const FSSpec *spec) +{ + FInfo info; + assert(spec); + + if (noErr != FSpGetFInfo(spec, &info)) return; + + /* Set file type to 'Midi' if the file is _not_ an alias. */ + if ((info.fdType != midiType) && !(info.fdFlags & kIsAlias)) { + info.fdType = midiType; + FSpSetFInfo(spec, &info); + DEBUG(driver, 3) ("qtmidi: changed filetype to 'Midi'"); + } +} + + +/** + * Loads a MIDI file and returns it as a QuickTime Movie structure. + * + * @param *path String with the path of an existing MIDI file. + * @param *moov Pointer to a @c Movie where the result will be stored. + * @return Wether the file was loaded and the @c Movie successfully created. + */ +static bool LoadMovieForMIDIFile(const char *path, Movie *moov) +{ + int fd; + int ret; + char magic[4]; + FSSpec fsspec; + short refnum = 0; + short resid = 0; + + assert(path); + assert(moov); + + DEBUG(driver, 2) ("qtmidi: begin loading '%s'...", path); + + /* + * XXX Manual check for MIDI header ('MThd'), as I don't know how to make + * QuickTime load MIDI files without a .mid suffix without knowing it's + * a MIDI file and setting the OSType of the file to the 'Midi' value. + * Perhahaps ugly, but it seems that it does the Right Thing(tm). + */ + if ((fd = open(path, O_RDONLY, 0)) == -1) + return false; + ret = read(fd, magic, 4); + close(fd); + if (ret < 4) return false; + + DEBUG(driver, 3) ("qtmidi: header is '%c%c%c%c'", + magic[0], magic[1], magic[2], magic[3]); + if (magic[0] != 'M' || magic[1] != 'T' || magic[2] != 'h' || magic[3] != 'd') + return false; + + if (!PathToFSSpec(path, &fsspec)) + return false; + SetMIDITypeIfNeeded(&fsspec); + + if (noErr != OpenMovieFile(&fsspec, &refnum, fsRdPerm)) + return false; + DEBUG(driver, 1) ("qtmidi: '%s' successfully opened", path); + + if (noErr != NewMovieFromFile(moov, refnum, &resid, NULL, + newMovieActive | newMovieDontAskUnresolvedDataRefs, NULL)) + { + CloseMovieFile(refnum); + return false; + } + DEBUG(driver, 2) ("qtmidi: movie container created"); + + CloseMovieFile(refnum); + return true; +} + + +/** + * Flag which has the @c true value when QuickTime is available and + * initialized. + */ +static bool _quicktime_started = false; + + +/** + * Initialize QuickTime if needed. This function sets the + * #_quicktime_started flag to @c true if QuickTime is present in the system + * and it was initialized properly. + */ +static void InitQuickTimeIfNeeded(void) +{ + OSStatus dummy; + + if (_quicktime_started) return; + + DEBUG(driver, 2) ("qtmidi: trying to initialize Quicktime"); + /* Be polite: check wether QuickTime is available and initialize it. */ + _quicktime_started = + (noErr == Gestalt(gestaltQuickTime, &dummy)) && + (noErr == EnterMovies()); + DEBUG(driver, 1) ("qtmidi: Quicktime was %s initialized", + _quicktime_started ? "successfully" : "NOT"); +} + + +/** Possible states of the QuickTime music driver. */ +enum +{ + QT_STATE_IDLE, /**< No file loaded. */ + QT_STATE_PLAY, /**< File loaded, playing. */ + QT_STATE_STOP, /**< File loaded, stopped. */ +}; + + +static Movie _quicktime_movie; /**< Current QuickTime @c Movie. */ +static byte _quicktime_volume = 127; /**< Current volume. */ +static int _quicktime_state = QT_STATE_IDLE; /**< Current player state. */ + + +/** + * Maps OpenTTD volume to QuickTime notion of volume. + */ +#define VOLUME ((short)((0x00FF & _quicktime_volume) << 1)) + + +static void StopSong(void); + + +/** + * Initialized the MIDI player, including QuickTime initialization. + * + * @todo Give better error messages by inspecting error codes returned by + * @c Gestalt() and @c EnterMovies(). Needs changes in + * #InitQuickTimeIfNeeded. + */ +static const char* StartDriver(const char * const *parm) +{ + InitQuickTimeIfNeeded(); + return (_quicktime_started) ? NULL : "can't initialize QuickTime"; +} + + +/** + * Checks wether the player is active. + * + * This function is called at regular intervals from OpenTTD's main loop, so + * we call @c MoviesTask() from here to let QuickTime do its work. + */ +static bool SongIsPlaying(void) +{ + if (!_quicktime_started) return true; + + switch (_quicktime_state) { + case QT_STATE_IDLE: + case QT_STATE_STOP: + /* Do nothing. */ + break; + case QT_STATE_PLAY: + MoviesTask(_quicktime_movie, 0); + /* Check wether movie ended. */ + if (IsMovieDone(_quicktime_movie) || + (GetMovieTime(_quicktime_movie, NULL) >= + GetMovieDuration(_quicktime_movie))) + _quicktime_state = QT_STATE_STOP; + } + + return (_quicktime_state == QT_STATE_PLAY); +} + + +/** + * Stops the MIDI player. + * + * Stops playing and frees any used resources before returning. As it + * deinitilizes QuickTime, the #_quicktime_started flag is set to @c false. + */ +static void StopDriver(void) +{ + if (!_quicktime_started) return; + + DEBUG(driver, 2) ("qtmidi: trying to stop driver..."); + switch (_quicktime_state) { + case QT_STATE_IDLE: + DEBUG(driver, 3) ("qtmidi: nothing to do (already idle)"); + /* Do nothing. */ + break; + case QT_STATE_PLAY: + StopSong(); + case QT_STATE_STOP: + DisposeMovie(_quicktime_movie); + } + + ExitMovies(); + _quicktime_started = false; + DEBUG(driver, 1) ("qtmidi: driver successfully stopped"); +} + + +/** + * Starts playing a new song. + * + * @param filename Path to a MIDI file. + */ +static void PlaySong(const char *filename) +{ + if (!_quicktime_started) return; + + DEBUG(driver, 3) ("qtmidi: request playing of '%s'n", filename); + switch (_quicktime_state) { + case QT_STATE_PLAY: + StopSong(); + DEBUG(driver, 2) ("qtmidi: previous tune stopped"); + /* XXX Fall-through -- no break needed. */ + case QT_STATE_STOP: + DisposeMovie(_quicktime_movie); + DEBUG(driver, 2) ("qtmidi: previous tune disposed"); + _quicktime_state = QT_STATE_IDLE; + /* XXX Fall-through -- no break needed. */ + case QT_STATE_IDLE: + LoadMovieForMIDIFile(filename, &_quicktime_movie); + SetMovieVolume(_quicktime_movie, VOLUME); + StartMovie(_quicktime_movie); + _quicktime_state = QT_STATE_PLAY; + } + DEBUG(driver, 1) ("qtmidi: playing '%s'", filename); +} + + +/** + * Stops playing the current song, if the player is active. + */ +static void StopSong(void) +{ + if (!_quicktime_started) return; + + switch (_quicktime_state) { + case QT_STATE_IDLE: + /* XXX Fall-through -- no break needed. */ + case QT_STATE_STOP: + DEBUG(driver, 2) ("qtmidi: stop requested, but already idle"); + /* Do nothing. */ + break; + case QT_STATE_PLAY: + StopMovie(_quicktime_movie); + _quicktime_state = QT_STATE_STOP; + DEBUG(driver, 1) ("qtmidi: player stopped"); + } +} + + +/** + * Changes the playing volume of the MIDI player. + * + * As QuickTime controls volume in a per-movie basis, the desired volume is + * stored in #_quicktime_volume, and the volume is set here using the + * #VOLUME macro, @b and when loading new song in #PlaySong. + * + * @param vol The desired volume, range of the value is @c 0-127 + */ +static void SetVolume(byte vol) +{ + if (!_quicktime_started) return; + + _quicktime_volume = vol; + + DEBUG(driver, 3) ("qtmidi: set volume to %u (%hi)", vol, VOLUME); + switch (_quicktime_state) { + case QT_STATE_IDLE: + /* Do nothing. */ + break; + case QT_STATE_PLAY: + case QT_STATE_STOP: + SetMovieVolume(_quicktime_movie, VOLUME); + } +} + + +/** + * Table of callbacks that implement the QuickTime MIDI player. + */ +const HalMusicDriver _qtime_music_driver = { + StartDriver, + StopDriver, + PlaySong, + StopSong, + SongIsPlaying, + SetVolume, +}; + + diff --git a/music/qtmidi.h b/music/qtmidi.h new file mode 100644 index 000000000..c640ab34b --- /dev/null +++ b/music/qtmidi.h @@ -0,0 +1,15 @@ +/* + * qtmidi.h + * + * $Id$ + */ + +#ifndef MUSIC_MACOSX_QUICKTIME_H +#define MUSIC_MACOSX_QUICKTIME_H + +#include "../hal.h" + +extern const HalMusicDriver _qtime_music_driver; + +#endif /* !MUSIC_MACOSX_QUICKTIME_H */ + |