diff --git a/bin/baseset/orig_win.obm b/bin/baseset/orig_win.obm index c8c4923..b02c2d5 100644 --- a/bin/baseset/orig_win.obm +++ b/bin/baseset/orig_win.obm @@ -152,7 +152,7 @@ GM_TT00.GM = 768:53760 ; followed by a solo repeat. This isn't in the original DOS version music and is likely ; unintentional from the people who converted the music from the DOS version. ; Actual song ends after measure 152. -GM_TT10.GM = 0:235008 +GM_TT10.GM = 0:233486 [origin] default = You can find it on your Transport Tycoon Deluxe CD-ROM. diff --git a/source.list b/source.list index df35cdd..c71a1c7 100644 --- a/source.list +++ b/source.list @@ -284,6 +284,7 @@ newgrf_townname.h news_func.h news_gui.h news_type.h +music/midifile.hpp music/null_m.h sound/null_s.h video/null_v.h @@ -1100,6 +1101,7 @@ video/null_v.cpp #end #end music/null_m.cpp +music/midifile.cpp #if DEDICATED #else #if WIN32 diff --git a/src/music/midifile.cpp b/src/music/midifile.cpp new file mode 100644 index 0000000..2df33bd --- /dev/null +++ b/src/music/midifile.cpp @@ -0,0 +1,432 @@ +/* $Id$ */ + +/* +* This file is part of OpenTTD. +* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. +* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . +*/ + +/* @file midifile.cpp Parser for standard MIDI files */ + +#include "midifile.hpp" +#include "../fileio_func.h" +#include "../fileio_type.h" +#include + + +/* implementation based on description at: http://www.somascape.org/midi/tech/mfile.html */ + + +/** + * Owning byte buffer readable as a stream. + * RAII-compliant to make teardown in error situations easier. + */ +class ByteBuffer { + byte *buf; + size_t buflen; + size_t pos; +public: + /** + * Construct buffer from data in a file. + * If file does not have sufficient bytes available, the object is constructed + * in an error state, that causes all further function calls to fail. + * @param file file to read from at current position + * @param len number of bytes to read + */ + ByteBuffer(FILE *file, size_t len) { + this->buf = MallocT(len); + if (fread(this->buf, 1, len, file) == len) { + this->buflen = len; + this->pos = 0; + } else { + /* invalid state */ + this->buflen = 0; + } + } + + /** + * Destructor, frees the buffer. + */ + ~ByteBuffer() { + free(this->buf); + } + + /** + * Return whether the buffer was constructed successfully. + * @return true is the buffer contains data + */ + bool IsValid() const { + return this->buflen > 0; + } + + /** + * Return whether reading has reached the end of the buffer. + * @return true if there are no more bytes available to read + */ + bool IsEnd() const { + return this->pos >= this->buflen; + } + + /** + * Read a single byte from the buffer. + * @param[out] b returns the read value + * @return true if a byte was available for reading + */ + bool ReadByte(byte &b) { + if (this->IsEnd()) return false; + b = this->buf[this->pos++]; + return true; + } + + /** + * Read a MIDI file variable length value. + * Each byte encodes 7 bits of the value, most-significant bits are encoded first. + * If the most significant bit in a byte is set, there are further bytes encoding the value. + * @param[out] res returns the read value + * @return true if there was data available + */ + bool ReadVariableLength(uint32 &res) { + res = 0; + byte b = 0; + do { + if (this->IsEnd()) return false; + b = this->buf[this->pos++]; + res = (res << 7) | (b & 0x7F); + } while (b & 0x80); + return true; + } + + /** + * Read bytes into a buffer. + * @param[out] dest buffer to copy info + * @param length number of bytes to read + * @return true if the requested number of bytes were available + */ + bool ReadBuffer(byte *dest, size_t length) { + if (this->IsEnd()) return false; + if (this->buflen - this->pos < length) return false; + memcpy(dest, this->buf + this->pos, length); + this->pos += length; + return true; + } + + /** + * Skip over a number of bytes in the buffer. + * @param count number of bytes to skip over + * @return true if there were enough bytes available + */ + bool Skip(size_t count) { + if (this->IsEnd()) return false; + if (this->buflen - this->pos < count) return false; + this->pos += count; + return true; + } + + /** + * Go a number of bytes back to re-read. + * @param count number of bytes to go back + * @return true if at least count bytes had been read previously + */ + bool Rewind(size_t count) { + if (count > this->pos) return false; + this->pos -= count; + return true; + } +}; + +static bool ReadTrackChunk(FILE *file, MidiFile &target) { + byte buf[4]; + + const byte magic[] = { 'M', 'T', 'r', 'k' }; + if (fread(buf, sizeof(magic), 1, file) != 1) + return false; + if (memcmp(magic, buf, sizeof(magic)) != 0) + return false; + + /* read chunk length and then the whole chunk */ + if (fread(buf, 1, 4, file) != 4) return false; + size_t chunk_length = buf[3] | (buf[2] << 8) | (buf[1] << 16) | (buf[0] << 24); + + size_t file_pos = ftell(file); + ByteBuffer chunk(file, chunk_length); + if (!chunk.IsValid()) { + return false; + } + + target.blocks.push_back(MidiFile::DataBlock()); + MidiFile::DataBlock *block = &target.blocks.back(); + + byte last_status = 0; + bool running_sysex = false; + while (!chunk.IsEnd()) { + /* read deltatime for event, start new block */ + uint32 deltatime = 0; + if (!chunk.ReadVariableLength(deltatime)) { + return false; + } + if (deltatime > 0) { + target.blocks.push_back(MidiFile::DataBlock(block->ticktime + deltatime)); + block = &target.blocks.back(); + } + + /* read status byte */ + byte status; + if (!chunk.ReadByte(status)) { + return false; + } + + if ((status & 0x80) == 0) { + /* high bit not set means running status message, status is same as last + * convert to explicit status */ + chunk.Rewind(1); + status = last_status; + goto running_status; + } + else if ((status & 0xF0) != 0xF0) { + /* regular channel message */ + last_status = status; + running_status: + byte *data; + switch (status & 0xF0) { + case 0x80: case 0x90: + case 0xA0: case 0xB0: + case 0xE0: + /* 3 byte messages */ + data = block->data.Append(3); + data[0] = status; + if (!chunk.ReadBuffer(&data[1], 2)) { + return false; + } + break; + case 0xC0: case 0xD0: + /* 2 byte messages */ + data = block->data.Append(2); + data[0] = status; + if (!chunk.ReadByte(data[1])) { + return false; + } + break; + default: + NOT_REACHED(); + } + } + else if (status == 0xFF) { + /* meta event, read event type byte and data length */ + if (!chunk.ReadByte(buf[0])) { + return false; + } + uint32 length = 0; + if (!chunk.ReadVariableLength(length)) { + return false; + } + switch (buf[0]) { + case 0x2F: + /* end of track, no more data */ + if (length != 0) + return false; + else + return true; + case 0x51: + /* tempo change */ + if (length != 3) + return false; + if (!chunk.ReadBuffer(buf, 3)) return false; + target.tempos.push_back(MidiFile::TempoChange(block->ticktime, buf[0] << 16 | buf[1] << 8 | buf[2])); + break; + default: + /* unimportant meta event, skip over it */ + if (!chunk.Skip(length)) { + return false; + } + break; + } + } + else if (status == 0xF0 || (status == 0xF7 && running_sysex)) { + /* system exclusive message */ + uint32 length = 0; + if (!chunk.ReadVariableLength(length)) return false; + byte *data = block->data.Append(length + 1); + data[0] = 0xF0; + if (!chunk.ReadBuffer(data + 1, length)) return false; + if (data[length] != 0xF7) { + /* engage Casio weirdo mode - convert to normal sysex */ + running_sysex = true; + *block->data.Append() = 0xF7; + } else { + running_sysex = false; + } + } + else if (status == 0xF7) { + /* escape sequence */ + uint32 length = 0; + if (!chunk.ReadVariableLength(length)) return false; + byte *data = block->data.Append(length); + if (!chunk.ReadBuffer(data, length)) return false; + } + else { + /* messages undefined in standard midi files: + * 0xF1 - MIDI time code quarter frame + * 0xF2 - Song position pointer + * 0xF3 - Song select + * 0xF4 - undefined/reserved + * 0xF5 - undefined/reserved + * 0xF6 - Tune request for analog synths + * 0xF8..0xFE - System real-time messages + */ + return false; + } + } + + NOT_REACHED(); +} + +template +bool TicktimeAscending(const T &a, const T &b) { + return a.ticktime < b.ticktime; +} + +static bool FixupMidiData(MidiFile &target) { + /* sort all tempo changes and events */ + std::sort(target.tempos.begin(), target.tempos.end(), TicktimeAscending); + std::sort(target.blocks.begin(), target.blocks.end(), TicktimeAscending); + + if (target.tempos.size() == 0) { + /* no tempo information, assume 120 bpm (500,000 microseconds per beat */ + target.tempos.push_back(MidiFile::TempoChange(0, 500000)); + } + /* add sentinel tempo at end */ + target.tempos.push_back(MidiFile::TempoChange(UINT32_MAX, 0)); + + /* merge blocks with identical tick times */ + std::vector merged_blocks; + uint32 last_ticktime = 0; + for (size_t i = 0; i < target.blocks.size(); i++) { + MidiFile::DataBlock &block = target.blocks[i]; + if (block.ticktime > last_ticktime || merged_blocks.size() == 0) { + merged_blocks.push_back(block); + last_ticktime = block.ticktime; + } else { + byte *datadest = merged_blocks.back().data.Append(block.data.Length()); + memcpy(datadest, block.data.Begin(), block.data.Length()); + } + } + std::swap(merged_blocks, target.blocks); + + /* annotate blocks with real time */ + last_ticktime = 0; + uint32 last_realtime = 0; + MidiFile::TempoChange *current_tempo = &target.tempos[0]; + for (size_t i = 0; i < target.blocks.size(); i++) { + MidiFile::DataBlock &block = target.blocks[i]; + uint32 deltaticks = block.ticktime - last_ticktime; + /* check if deltatime gets sliced by up tempo change */ + if (block.ticktime <= current_tempo[1].ticktime) { + /* simple case */ + block.realtime = last_realtime + deltaticks * current_tempo->tempo / target.tickdiv; + } else { + /* yes, new tempo */ + MidiFile::TempoChange *new_tempo = current_tempo + 1; + block.realtime = last_realtime; + block.realtime += (new_tempo->ticktime - last_ticktime) * current_tempo->tempo / target.tickdiv; + block.realtime += (block.ticktime - new_tempo->ticktime) * new_tempo->tempo / target.tickdiv; + current_tempo = new_tempo; + } + last_ticktime = block.ticktime; + last_realtime = block.realtime; + } + + return true; +} + +/** + * Read the header of a standard MIDI file. + * @param[in] filename name of file to read from + * @param[out] header filled with data read + * @return true if the file could be opened and contained a header with correct format + */ +bool MidiFile::ReadSMFHeader(const char *filename, SMFHeader &header) { + FILE *file = FioFOpenFile(filename, "rb", Subdirectory::BASESET_DIR); + if (!file) return false; + bool result = ReadSMFHeader(file, header); + FioFCloseFile(file); + return result; +} + +/** + * Read the header of a standard MIDI file. + * The function will consume 14 bytes from the current file pointer position. + * @param[in] file open file to read from (should be in binary mode) + * @param[out] header filled with data read + * @return true if a header in correct format could be read from the file + */ +bool MidiFile::ReadSMFHeader(FILE *file, SMFHeader &header) { + /* try to read header, fixed size */ + byte buffer[14]; + if (fread(buffer, sizeof(buffer), 1, file) != 1) + return false; + + /* check magic, 'MThd' followed by 4 byte length indicator (always = 6 in SMF) */ + const byte magic[] = { 'M', 'T', 'h', 'd', 0x00, 0x00, 0x00, 0x06 }; + if (MemCmpT(buffer, magic, sizeof(magic)) != 0) + return false; + + /* read the parameters of the file */ + header.format = (buffer[8] << 8) | buffer[9]; + header.tracks = (buffer[10] << 8) | buffer[11]; + header.tickdiv = (buffer[12] << 8) | buffer[13]; + return true; +} + +/** + * Load a standard MIDI file. + * @param filename name of the file to load + * @returns true if loaded was successful + */ +bool MidiFile::LoadFile(const char *filename) { + this->blocks.clear(); + this->tempos.clear(); + this->tickdiv = 0; + + bool success = false; + FILE *file = FioFOpenFile(filename, "rb", Subdirectory::BASESET_DIR); + + SMFHeader header; + if (!ReadSMFHeader(file, header)) + goto cleanup; + + /* only format 0 (single-track) and format 1 (multi-track single-song) are accepted for now */ + if (header.format != 0 && header.format != 1) + goto cleanup; + /* doesn't support SMPTE timecode files */ + if ((header.tickdiv & 0x8000) != 0) + goto cleanup; + + this->tickdiv = header.tickdiv; + + for (; header.tracks > 0; header.tracks--) { + if (!ReadTrackChunk(file, *this)) + goto cleanup; + } + + success = FixupMidiData(*this); + +cleanup: + FioFCloseFile(file); + return success; +} + +/** + * Move data from other to this, and clears other. + * @param other object containing loaded data to take over + */ +void MidiFile::MoveFrom(MidiFile &other) { + std::swap(this->blocks, other.blocks); + std::swap(this->tempos, other.tempos); + this->tickdiv = other.tickdiv; + + other.blocks.clear(); + other.tempos.clear(); + other.tickdiv = 0; +} + diff --git a/src/music/midifile.hpp b/src/music/midifile.hpp new file mode 100644 index 0000000..58507cc --- /dev/null +++ b/src/music/midifile.hpp @@ -0,0 +1,43 @@ +/* $Id$ */ + +/* +* This file is part of OpenTTD. +* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. +* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . +*/ + +/* @file midifile.hpp Parser for standard MIDI files */ + +#include "../stdafx.h" +#include "../core/smallvec_type.hpp" +#include + +struct MidiFile { + struct DataBlock { + uint32 ticktime; ///< tick number since start of file this block should be triggered at + uint32 realtime; ///< real-time (microseconds) since start of file this block should be triggered at + SmallVector data; ///< raw midi data contained in block + DataBlock(uint32 _ticktime = 0) : ticktime(_ticktime) { } + }; + struct TempoChange { + uint32 ticktime; ///< tick number since start of file this tempo change occurs at + uint32 tempo; ///< new tempo in microseconds per tick + TempoChange(uint32 _ticktime, uint32 _tempo) : ticktime(_ticktime), tempo(_tempo) { } + }; + struct SMFHeader { + uint16 format; + uint16 tracks; + uint16 tickdiv; + }; + + std::vector blocks; ///< sequential time-annotated data of file, merged to a single track + std::vector tempos; ///< list of tempo changes in file + uint16 tickdiv; ///< ticks per quarter note + + bool LoadFile(const char *filename); + void MoveFrom(MidiFile &other); + + static bool ReadSMFHeader(const char *filename, SMFHeader &header); + static bool ReadSMFHeader(FILE *file, SMFHeader &header); +}; diff --git a/src/music/win32_m.cpp b/src/music/win32_m.cpp index e1e7f07..357f686 100644 --- a/src/music/win32_m.cpp +++ b/src/music/win32_m.cpp @@ -15,170 +15,352 @@ #include #include #include "../os/windows/win32.h" +#include "../debug.h" +#include "midifile.hpp" #include "../safeguards.h" +struct PlaybackSegment { + uint32 start, end; + size_t start_block; + bool loop; +}; + static struct { - bool stop_song; - bool terminate; - bool playing; - int new_vol; - HANDLE wait_obj; - HANDLE thread; - UINT_PTR devid; - char start_song[MAX_PATH]; + UINT time_period; + HMIDIOUT midi_out; + UINT timer_id; + CRITICAL_SECTION lock; + + bool playing; ///< flag indicating that playback is active + bool do_start; ///< flag for starting playback of next_file at next opportunity + bool do_stop; ///< flag for stopping playback at next opportunity + byte current_volume; ///< current effective volume setting + byte new_volume; ///< volume setting to change to + + MidiFile current_file; ///< file currently being played from + PlaybackSegment current_segment; ///< segment info for current playback + DWORD playback_start_time; ///< timestamp current file started playing back + size_t current_block; ///< next block index to send + MidiFile next_file; ///< upcoming file to play + PlaybackSegment next_segment; ///< segment info for upcoming file + + byte channel_volumes[16]; } _midi; static FMusicDriver_Win32 iFMusicDriver_Win32; -void MusicDriver_Win32::PlaySong(const char *filename, int time_start, int time_end, bool loop) -{ - assert(filename != NULL); - strecpy(_midi.start_song, filename, lastof(_midi.start_song)); - _midi.playing = true; - _midi.stop_song = false; - SetEvent(_midi.wait_obj); + +static byte ScaleVolume(byte original, byte scale) { + return original * scale / 127; } -void MusicDriver_Win32::StopSong() -{ - if (_midi.playing) { - _midi.stop_song = true; - _midi.start_song[0] = '\0'; - SetEvent(_midi.wait_obj); + +void CALLBACK MidiOutProc(HMIDIOUT hmo, UINT wMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2) { + if (wMsg == MOM_DONE) { + MIDIHDR *hdr = (LPMIDIHDR)dwParam1; + midiOutUnprepareHeader(hmo, hdr, sizeof(*hdr)); + free(hdr); } } -bool MusicDriver_Win32::IsSongPlaying() -{ - return _midi.playing; +static void TransmitChannelMsg(byte status, byte p1, byte p2 = 0) { + midiOutShortMsg(_midi.midi_out, status | (p1 << 8) | (p2 << 16)); } -void MusicDriver_Win32::SetVolume(byte vol) -{ - _midi.new_vol = vol; - SetEvent(_midi.wait_obj); +static void TransmitSysex(byte *&msg_start, size_t &remaining) { + /* find end of message */ + byte *msg_end = msg_start; + while (*msg_end != 0xF7) msg_end++; + msg_end++; /* also include sysex end byte */ + + /* prepare header */ + MIDIHDR *hdr = CallocT(1); + if (midiOutPrepareHeader(_midi.midi_out, hdr, sizeof(*hdr)) == MMSYSERR_NOERROR) { + /* transmit - just point directly into the data buffer */ + hdr->lpData = (LPSTR)msg_start; + hdr->dwBufferLength = msg_end - msg_start; + hdr->dwBytesRecorded = hdr->dwBufferLength; + midiOutLongMsg(_midi.midi_out, hdr, sizeof(*hdr)); + } else { + free(hdr); + } + + /* update position in buffer */ + remaining -= msg_end - msg_start; + msg_start = msg_end; } -static MCIERROR CDECL MidiSendCommand(const TCHAR *cmd, ...) -{ - va_list va; - TCHAR buf[512]; +void CALLBACK TimerCallback(UINT uTimerID, UINT, DWORD_PTR dwUser, DWORD_PTR, DWORD_PTR) { + if (TryEnterCriticalSection(&_midi.lock)) { + /* check for stop */ + if (_midi.do_stop) { + DEBUG(driver, 2, "Win32-MIDI: timer: do_stop is set"); + midiOutReset(_midi.midi_out); + _midi.playing = false; + _midi.do_stop = false; + LeaveCriticalSection(&_midi.lock); + return; + } - va_start(va, cmd); - _vsntprintf(buf, lengthof(buf), cmd, va); - va_end(va); - return mciSendString(buf, NULL, 0, 0); + /* check for start/restart/change song */ + if (_midi.do_start) { + DEBUG(driver, 2, "Win32-MIDI: timer: do_start is set"); + if (_midi.playing) { + midiOutReset(_midi.midi_out); + } + _midi.current_file.MoveFrom(_midi.next_file); + std::swap(_midi.next_segment, _midi.current_segment); + _midi.current_segment.start_block = 0; + _midi.playback_start_time = timeGetTime(); + _midi.playing = true; + _midi.do_start = false; + _midi.current_block = 0; + + MemSetT(_midi.channel_volumes, 127, lengthof(_midi.channel_volumes)); + } else if (!_midi.playing) { + /* not playing, stop the timer */ + DEBUG(driver, 2, "Win32-MIDI: timer: not playing, stopping timer"); + timeKillEvent(uTimerID); + _midi.timer_id = 0; + LeaveCriticalSection(&_midi.lock); + return; + } + + /* check for volume change */ + static int volume_throttle = 0; + if (_midi.current_volume != _midi.new_volume) { + if (volume_throttle == 0) { + DEBUG(driver, 2, "Win32-MIDI: timer: volume change"); + _midi.current_volume = _midi.new_volume; + volume_throttle = 20 / _midi.time_period; + for (int ch = 0; ch < 16; ch++) { + int vol = ScaleVolume(_midi.channel_volumes[ch], _midi.current_volume); + TransmitChannelMsg(0xB0 | ch, 0x07, vol); + } + } + else { + volume_throttle--; + } + } + + LeaveCriticalSection(&_midi.lock); + } + + if (_midi.current_segment.start > 0 && _midi.current_block == 0 && _midi.current_segment.start_block == 0) { + /* find first block after start time and pretend playback started earlier + * this is to allow all blocks prior to the actual start to still affect playback, + * as they may contain important controller and program changes */ + size_t preload_bytes = 0; + for (size_t bl = 0; bl < _midi.current_file.blocks.size(); bl++) { + MidiFile::DataBlock &block = _midi.current_file.blocks[bl]; + preload_bytes += block.data.Length(); + if (block.ticktime >= _midi.current_segment.start) { + if (_midi.current_segment.loop) { + DEBUG(driver, 2, "Win32-MIDI: timer: loop from block %d (ticktime %d, realtime %.3f, bytes %d)", (int)bl, (int)block.ticktime, ((int)block.realtime)/1000.0, (int)preload_bytes); + _midi.current_segment.start_block = bl; + break; + } else { + DEBUG(driver, 2, "Win32-MIDI: timer: start from block %d (ticktime %d, realtime %.3f, bytes %d)", (int)bl, (int)block.ticktime, ((int)block.realtime) / 1000.0, (int)preload_bytes); + _midi.playback_start_time -= block.realtime / 1000; + break; + } + } + } + } + + + /* play pending blocks */ + DWORD current_time = timeGetTime(); + DWORD playback_time = current_time - _midi.playback_start_time; + while (_midi.current_block < _midi.current_file.blocks.size()) { + MidiFile::DataBlock &block = _midi.current_file.blocks[_midi.current_block]; + + /* check that block is not in the future */ + if (block.realtime / 1000 > playback_time) + break; + /* check that block isn't at end-of-song override */ + if (_midi.current_segment.end > 0 && block.ticktime >= _midi.current_segment.end) { + if (_midi.current_segment.loop) { + _midi.current_block = _midi.current_segment.start_block; + _midi.playback_start_time = timeGetTime() - _midi.current_file.blocks[_midi.current_block].realtime / 1000; + } else { + _midi.do_stop = true; + } + break; + } + + byte *data = block.data.Begin(); + size_t remaining = block.data.Length(); + byte last_status = 0; + while (remaining > 0) { + /* MidiFile ought to have converted everything out of running status, + * but handle it anyway just to be safe */ + byte status = data[0]; + if (status & 0x80) { + last_status = status; + data++; + remaining--; + } else { + status = last_status; + } + switch (status & 0xF0) { + case 0xC0: case 0xD0: + /* 2 byte channel messages */ + TransmitChannelMsg(status, data[0]); + data++; + remaining--; + break; + case 0x80: case 0x90: case 0xA0: case 0xE0: + /* 3 byte channel messages */ + TransmitChannelMsg(status, data[0], data[1]); + data += 2; + remaining -= 2; + break; + case 0xB0: + /* controller change */ + if (data[0] == 0x07) { + /* volume controller, adjust for user volume */ + _midi.channel_volumes[status & 0x0F] = data[1]; + int vol = ScaleVolume(data[1], _midi.current_volume); + TransmitChannelMsg(status, data[0], vol); + } else { + /* handle other controllers normally */ + TransmitChannelMsg(status, data[0], data[1]); + } + data += 2; + remaining -= 2; + break; + case 0xF0: + /* system messages */ + switch (status) { + case 0xF0: /* system exclusive */ + TransmitSysex(data, remaining); + break; + case 0xF1: /* time code quarter frame */ + case 0xF3: /* song select */ + data++; + remaining--; + break; + case 0xF2: /* song position pointer */ + data += 2; + remaining -= 2; + break; + default: /* remaining have no data bytes */ + break; + } + break; + } + } + + _midi.current_block++; + } + + if (_midi.current_block == _midi.current_file.blocks.size()) { + if (_midi.current_segment.loop) { + _midi.current_block = 0; + _midi.playback_start_time = timeGetTime(); + } else { + _midi.do_stop = true; + } + } } -static bool MidiIntPlaySong(const char *filename) +void MusicDriver_Win32::PlaySong(const char *filename, int time_start, int time_end, bool loop) { - MidiSendCommand(_T("close all")); + DEBUG(driver, 2, "Win32-MIDI: PlaySong: entry"); + EnterCriticalSection(&_midi.lock); + + _midi.next_file.LoadFile(filename); + _midi.next_segment.start = time_start; + _midi.next_segment.end = time_end; + _midi.next_segment.loop = loop; + + DEBUG(driver, 2, "Win32-MIDI: PlaySong: setting flag"); + _midi.do_start = true; - if (MidiSendCommand(_T("open \"%s\" type sequencer alias song"), OTTD2FS(filename)) != 0) { - /* Let's try the "short name" */ - TCHAR buf[MAX_PATH]; - if (GetShortPathName(OTTD2FS(filename), buf, MAX_PATH) == 0) return false; - if (MidiSendCommand(_T("open \"%s\" type sequencer alias song"), buf) != 0) return false; + if (_midi.timer_id == 0) { + DEBUG(driver, 2, "Win32-MIDI: PlaySong: starting timer"); + _midi.timer_id = timeSetEvent(_midi.time_period, _midi.time_period, TimerCallback, (DWORD_PTR)this, TIME_PERIODIC | TIME_CALLBACK_FUNCTION); } - MidiSendCommand(_T("seek song to start wait")); - return MidiSendCommand(_T("play song")) == 0; + LeaveCriticalSection(&_midi.lock); } -static void MidiIntStopSong() +void MusicDriver_Win32::StopSong() { - MidiSendCommand(_T("close all")); + DEBUG(driver, 2, "Win32-MIDI: StopSong: entry"); + EnterCriticalSection(&_midi.lock); + DEBUG(driver, 2, "Win32-MIDI: StopSong: setting flag"); + _midi.do_stop = true; + LeaveCriticalSection(&_midi.lock); } -static void MidiIntSetVolume(int vol) +bool MusicDriver_Win32::IsSongPlaying() { - DWORD v = (vol * 65535 / 127); - midiOutSetVolume((HMIDIOUT)_midi.devid, v + (v << 16)); + return _midi.playing || _midi.do_start; } -static bool MidiIntIsSongPlaying() +void MusicDriver_Win32::SetVolume(byte vol) { - char buf[16]; - mciSendStringA("status song mode", buf, sizeof(buf), 0); - return strcmp(buf, "playing") == 0 || strcmp(buf, "seeking") == 0; + EnterCriticalSection(&_midi.lock); + _midi.new_volume = vol; + LeaveCriticalSection(&_midi.lock); } -static DWORD WINAPI MidiThread(LPVOID arg) +const char *MusicDriver_Win32::Start(const char * const *parm) { - SetWin32ThreadName(-1, "ottd:win-midi"); + DEBUG(driver, 2, "Win32-MIDI: Start: initializing"); - do { - char *s; - int vol; + InitializeCriticalSection(&_midi.lock); - vol = _midi.new_vol; - if (vol != -1) { - _midi.new_vol = -1; - MidiIntSetVolume(vol); - } - - s = _midi.start_song; - if (s[0] != '\0') { - _midi.playing = MidiIntPlaySong(s); - s[0] = '\0'; - - /* Delay somewhat in case we don't manage to play. */ - if (!_midi.playing) WaitForMultipleObjects(1, &_midi.wait_obj, FALSE, 5000); - } + int resolution = GetDriverParamInt(parm, "resolution", 5); + int port = GetDriverParamInt(parm, "port", -1); - if (_midi.stop_song && _midi.playing) { - _midi.stop_song = false; - _midi.playing = false; - MidiIntStopSong(); - } + UINT devid; + if (port < 0) { + devid = MIDI_MAPPER; + } else { + devid = (UINT)port; + } - if (_midi.playing && !MidiIntIsSongPlaying()) _midi.playing = false; + resolution = Clamp(resolution, 1, 20); - WaitForMultipleObjects(1, &_midi.wait_obj, FALSE, 1000); - } while (!_midi.terminate); + if (midiOutOpen(&_midi.midi_out, devid, (DWORD_PTR)&MidiOutProc, (DWORD_PTR)this, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) + return "could not open midi device"; - MidiIntStopSong(); - return 0; -} + { + static byte gm_enable_sysex[] = { 0xF0, 0x7E, 0x00, 0x09, 0x01, 0xF7 }; + byte *ptr = gm_enable_sysex; + size_t len = sizeof(gm_enable_sysex); + TransmitSysex(ptr, len); + } -const char *MusicDriver_Win32::Start(const char * const *parm) -{ - MIDIOUTCAPS midicaps; - UINT nbdev; - UINT_PTR dev; - char buf[16]; - - mciSendStringA("capability sequencer has audio", buf, lengthof(buf), 0); - if (strcmp(buf, "true") != 0) return "MCI sequencer can't play audio"; - - memset(&_midi, 0, sizeof(_midi)); - _midi.new_vol = -1; - - /* Get midi device */ - _midi.devid = MIDI_MAPPER; - for (dev = 0, nbdev = midiOutGetNumDevs(); dev < nbdev; dev++) { - if (midiOutGetDevCaps(dev, &midicaps, sizeof(midicaps)) == 0 && (midicaps.dwSupport & MIDICAPS_VOLUME)) { - _midi.devid = dev; - break; + TIMECAPS timecaps; + if (timeGetDevCaps(&timecaps, sizeof(timecaps)) == MMSYSERR_NOERROR) { + _midi.time_period = min(max((UINT)resolution, timecaps.wPeriodMin), timecaps.wPeriodMax); + if (timeBeginPeriod(_midi.time_period) == MMSYSERR_NOERROR) { + DEBUG(driver, 2, "Win32-MIDI: Start: timer resolution is %d", (int)_midi.time_period); + return NULL; } } - - if (NULL == (_midi.wait_obj = CreateEvent(NULL, FALSE, FALSE, NULL))) return "Failed to create event"; - - /* The lpThreadId parameter of CreateThread (the last parameter) - * may NOT be NULL on Windows 95, 98 and ME. */ - DWORD threadId; - if (NULL == (_midi.thread = CreateThread(NULL, 8192, MidiThread, 0, 0, &threadId))) return "Failed to create thread"; - - return NULL; + midiOutClose(_midi.midi_out); + return "could not set timer resolution"; } void MusicDriver_Win32::Stop() { - _midi.terminate = true; - SetEvent(_midi.wait_obj); - WaitForMultipleObjects(1, &_midi.thread, true, INFINITE); - CloseHandle(_midi.wait_obj); - CloseHandle(_midi.thread); + EnterCriticalSection(&_midi.lock); + + if (_midi.timer_id) { + timeKillEvent(_midi.timer_id); + _midi.timer_id = 0; + } + + timeEndPeriod(_midi.time_period); + midiOutReset(_midi.midi_out); + midiOutClose(_midi.midi_out); + + LeaveCriticalSection(&_midi.lock); + DeleteCriticalSection(&_midi.lock); }