Created
March 11, 2018 09:35
-
-
Save anonymous/bd4e402012e9afd32303c62638ef3261 to your computer and use it in GitHub Desktop.
Music playback patches for OpenTTD r27967
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/bin/baseset/orig_win.obm b/bin/baseset/orig_win.obm | |
index 8e2053e..b02c2d5 100644 | |
--- a/bin/baseset/orig_win.obm | |
+++ b/bin/baseset/orig_win.obm | |
@@ -142,5 +142,17 @@ GM_TT19.GM = Funk Central | |
GM_TT20.GM = Jammit | |
GM_TT21.GM = Movin' On | |
+; MIDI timecodes where the playback should attemp to start and stop short. | |
+; This is to allow fixing undesired silences in original MIDI files. | |
+; However not all music drivers may support this. | |
+[timingtrim] | |
+; Theme has two beats silence at the beginning which prevents clean looping. | |
+GM_TT00.GM = 768:53760 | |
+; Can't Get There From Here from the Windows version has a long silence at the end, | |
+; 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: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/base_media_base.h b/src/base_media_base.h | |
index d5de6c3..43928dd 100644 | |
--- a/src/base_media_base.h | |
+++ b/src/base_media_base.h | |
@@ -286,6 +286,9 @@ struct MusicSet : BaseSet<MusicSet, NUM_SONGS_AVAILABLE, false> { | |
char song_name[NUM_SONGS_AVAILABLE][32]; | |
byte track_nr[NUM_SONGS_AVAILABLE]; | |
byte num_available; | |
+ int override_start[NUM_SONGS_AVAILABLE]; | |
+ int override_end[NUM_SONGS_AVAILABLE]; | |
+ bool has_theme; | |
bool FillSetDetails(struct IniFile *ini, const char *path, const char *full_filename); | |
}; | |
diff --git a/src/music.cpp b/src/music.cpp | |
index 4001e62..4511ce6 100644 | |
--- a/src/music.cpp | |
+++ b/src/music.cpp | |
@@ -65,8 +65,13 @@ bool MusicSet::FillSetDetails(IniFile *ini, const char *path, const char *full_f | |
bool ret = this->BaseSet<MusicSet, NUM_SONGS_AVAILABLE, false>::FillSetDetails(ini, path, full_filename); | |
if (ret) { | |
this->num_available = 0; | |
+ this->has_theme = !StrEmpty(this->files[0].filename); | |
+ | |
+ uint next_track_nr = 1; | |
+ | |
IniGroup *names = ini->GetGroup("names"); | |
- for (uint i = 0, j = 1; i < lengthof(this->song_name); i++) { | |
+ IniGroup *timingtrim = ini->GetGroup("timingtrim"); | |
+ for (uint i = 0; i < lengthof(this->song_name); i++) { | |
const char *filename = this->files[i].filename; | |
if (names == NULL || StrEmpty(filename)) { | |
this->song_name[i][0] = '\0'; | |
@@ -74,15 +79,16 @@ bool MusicSet::FillSetDetails(IniFile *ini, const char *path, const char *full_f | |
} | |
IniItem *item = NULL; | |
+ const char *trimmed_filename = filename; | |
/* As we possibly add a path to the filename and we compare | |
* on the filename with the path as in the .obm, we need to | |
* keep stripping path elements until we find a match. */ | |
- for (const char *p = filename; p != NULL; p = strchr(p, PATHSEPCHAR)) { | |
+ for (; trimmed_filename != NULL; trimmed_filename = strchr(trimmed_filename, PATHSEPCHAR)) { | |
/* Remove possible double path separator characters from | |
* the beginning, so we don't start reading e.g. root. */ | |
- while (*p == PATHSEPCHAR) p++; | |
+ while (*trimmed_filename == PATHSEPCHAR) trimmed_filename++; | |
- item = names->GetItem(p, false); | |
+ item = names->GetItem(trimmed_filename, false); | |
if (item != NULL && !StrEmpty(item->value)) break; | |
} | |
@@ -92,8 +98,18 @@ bool MusicSet::FillSetDetails(IniFile *ini, const char *path, const char *full_f | |
} | |
strecpy(this->song_name[i], item->value, lastof(this->song_name[i])); | |
- this->track_nr[i] = j++; | |
this->num_available++; | |
+ /* if there is a theme song, number that one zero */ | |
+ this->track_nr[i] = (i==0 && this->has_theme) ? 0 : next_track_nr++; | |
+ | |
+ item = timingtrim->GetItem(trimmed_filename, false); | |
+ if (item != NULL && !StrEmpty(item->value)) { | |
+ const char *endpos = strchr(item->value, ':'); | |
+ if (endpos != NULL) { | |
+ this->override_start[i] = atoi(item->value); | |
+ this->override_end[i] = atoi(endpos + 1); | |
+ } | |
+ } | |
} | |
} | |
return ret; | |
diff --git a/src/music/allegro_m.cpp b/src/music/allegro_m.cpp | |
index 77b4881..d434304 100644 | |
--- a/src/music/allegro_m.cpp | |
+++ b/src/music/allegro_m.cpp | |
@@ -58,7 +58,7 @@ void MusicDriver_Allegro::Stop() | |
if (--_allegro_instance_count == 0) allegro_exit(); | |
} | |
-void MusicDriver_Allegro::PlaySong(const char *filename) | |
+void MusicDriver_Allegro::PlaySong(const char *filename, int time_start, int time_end, bool loop) | |
{ | |
if (_midi != NULL) destroy_midi(_midi); | |
_midi = load_midi(filename); | |
diff --git a/src/music/allegro_m.h b/src/music/allegro_m.h | |
index 69cf595..68cac0d 100644 | |
--- a/src/music/allegro_m.h | |
+++ b/src/music/allegro_m.h | |
@@ -21,7 +21,7 @@ public: | |
/* virtual */ void Stop(); | |
- /* virtual */ void PlaySong(const char *filename); | |
+ /* virtual */ void PlaySong(const char *filename, int time_start = 0, int time_end = 0, bool loop = false); | |
/* virtual */ void StopSong(); | |
diff --git a/src/music/bemidi.cpp b/src/music/bemidi.cpp | |
index 2bc2074..a34e5a1 100644 | |
--- a/src/music/bemidi.cpp | |
+++ b/src/music/bemidi.cpp | |
@@ -34,7 +34,7 @@ void MusicDriver_BeMidi::Stop() | |
midiSynthFile.UnloadFile(); | |
} | |
-void MusicDriver_BeMidi::PlaySong(const char *filename) | |
+void MusicDriver_BeMidi::PlaySong(const char *filename, int time_start, int time_end, bool loop) | |
{ | |
this->Stop(); | |
entry_ref midiRef; | |
diff --git a/src/music/bemidi.h b/src/music/bemidi.h | |
index 23c6249..fb8ce1c 100644 | |
--- a/src/music/bemidi.h | |
+++ b/src/music/bemidi.h | |
@@ -21,7 +21,7 @@ public: | |
/* virtual */ void Stop(); | |
- /* virtual */ void PlaySong(const char *filename); | |
+ /* virtual */ void PlaySong(const char *filename, int time_start = 0, int time_end = 0, bool loop = false); | |
/* virtual */ void StopSong(); | |
diff --git a/src/music/cocoa_m.cpp b/src/music/cocoa_m.cpp | |
index 925dc21..b68ee6e 100644 | |
--- a/src/music/cocoa_m.cpp | |
+++ b/src/music/cocoa_m.cpp | |
@@ -143,7 +143,7 @@ void MusicDriver_Cocoa::Stop() | |
* | |
* @param filename Path to a MIDI file. | |
*/ | |
-void MusicDriver_Cocoa::PlaySong(const char *filename) | |
+void MusicDriver_Cocoa::PlaySong(const char *filename, int time_start, int time_end, bool loop) | |
{ | |
DEBUG(driver, 2, "cocoa_m: trying to play '%s'", filename); | |
diff --git a/src/music/cocoa_m.h b/src/music/cocoa_m.h | |
index 1963bef..cc9d453 100644 | |
--- a/src/music/cocoa_m.h | |
+++ b/src/music/cocoa_m.h | |
@@ -20,7 +20,7 @@ public: | |
/* virtual */ void Stop(); | |
- /* virtual */ void PlaySong(const char *filename); | |
+ /* virtual */ void PlaySong(const char *filename, int time_start = 0, int time_end = 0, bool loop = false); | |
/* virtual */ void StopSong(); | |
diff --git a/src/music/dmusic.cpp b/src/music/dmusic.cpp | |
index de3bda8..454d886 100644 | |
--- a/src/music/dmusic.cpp | |
+++ b/src/music/dmusic.cpp | |
@@ -66,6 +66,151 @@ struct ProcPtrs { | |
static ProcPtrs proc; | |
+/** | |
+ * Adjust the volume of a playing MIDI file | |
+ */ | |
+class VolumeControlTool : public IDirectMusicTool { | |
+public: | |
+ /* IUnknown */ | |
+ STDMETHOD(QueryInterface) (THIS_ REFIID iid, LPVOID FAR *outptr) { | |
+ if (iid == IID_IUnknown) | |
+ *outptr = (IUnknown*)this; | |
+ else if (iid == IID_IDirectMusicTool) | |
+ *outptr = (IDirectMusicTool*)this; | |
+ else | |
+ *outptr = NULL; | |
+ if (*outptr == NULL) | |
+ return E_NOINTERFACE; | |
+ else | |
+ return S_OK; | |
+ } | |
+ STDMETHOD_(ULONG, AddRef) (THIS) { | |
+ /* class is only used as a single static instance, doesn't need refcounting */ | |
+ return 1; | |
+ } | |
+ STDMETHOD_(ULONG, Release) (THIS) { | |
+ /* class is only used as a single static instance, doesn't need refcounting */ | |
+ return 1; | |
+ } | |
+ | |
+ /* IDirectMusicTool */ | |
+ STDMETHOD(Init) (THIS_ IDirectMusicGraph* pGraph) { | |
+ return S_OK; | |
+ } | |
+ STDMETHOD(GetMsgDeliveryType) (THIS_ DWORD* pdwDeliveryType) { | |
+ *pdwDeliveryType = DMUS_PMSGF_TOOL_QUEUE; | |
+ return S_OK; | |
+ } | |
+ STDMETHOD(GetMediaTypeArraySize) (THIS_ DWORD* pdwNumElements) { | |
+ *pdwNumElements = 1; | |
+ return S_OK; | |
+ } | |
+ STDMETHOD(GetMediaTypes) (THIS_ DWORD** padwMediaTypes, DWORD dwNumElements) { | |
+ if (dwNumElements < 1) | |
+ return S_FALSE; | |
+ *padwMediaTypes[0] = DMUS_PMSGT_MIDI; | |
+ return S_OK; | |
+ } | |
+ STDMETHOD(ProcessPMsg) (THIS_ IDirectMusicPerformance* pPerf, DMUS_PMSG* pPMSG) { | |
+ pPMSG->pGraph->StampPMsg(pPMSG); | |
+ if (pPMSG->dwType == DMUS_PMSGT_MIDI) { | |
+ DMUS_MIDI_PMSG *msg = (DMUS_MIDI_PMSG*)pPMSG; | |
+ if ((msg->bStatus & 0xF0) == 0xB0) { | |
+ /* controller change message */ | |
+ byte channel = msg->dwPChannel & 0x0F; /* technically wrong, but seems to hold for standard midi files */ | |
+ if (msg->bByte1 == 0x07) { | |
+ /* main volume for channel */ | |
+ if (msg->punkUser == this) { | |
+ /* if the user pointer is set to 'this', it's a sentinel message for user volume control change. | |
+ * in that case don't store, but just send the actual current adjusted channel volume */ | |
+ msg->punkUser = NULL; | |
+ } else { | |
+ this->current_controllers[channel] = msg->bByte2; | |
+ this->CalculateAdjustedControllers(); | |
+ DEBUG(driver, 2, "DirectMusic: song volume adjust ch=%2d before=%3d after=%3d (pch=%08x)", (int)channel, (int)msg->bByte2, (int)this->adjusted_controllers[channel], msg->dwPChannel); | |
+ } | |
+ msg->bByte2 = this->adjusted_controllers[channel]; | |
+ } else if (msg->bByte1 == 0x79) { | |
+ /* reset all controllers */ | |
+ this->current_controllers[channel] = 127; | |
+ this->adjusted_controllers[channel] = this->current_volume; | |
+ } | |
+ } | |
+ } | |
+ return DMUS_S_REQUEUE; | |
+ } | |
+ STDMETHOD(Flush) (THIS_ IDirectMusicPerformance* pPerf, DMUS_PMSG* pPMSG, REFERENCE_TIME rtTime) { | |
+ return DMUS_S_REQUEUE; | |
+ } | |
+private: | |
+ byte current_volume; | |
+ byte current_controllers[16]; | |
+ byte adjusted_controllers[16]; | |
+ | |
+ void CalculateAdjustedControllers() { | |
+ for (int ch = 0; ch < 16; ch++) { | |
+ this->adjusted_controllers[ch] = this->current_controllers[ch] * this->current_volume / 127; | |
+ } | |
+ } | |
+public: | |
+ static VolumeControlTool instance; | |
+ | |
+ VolumeControlTool() { | |
+ this->current_volume = 127; | |
+ for (int ch = 0; ch < 16; ch++) { | |
+ this->current_controllers[ch] = 127; | |
+ this->adjusted_controllers[ch] = 127; | |
+ } | |
+ } | |
+ | |
+ void SetVolume(byte new_volume) { | |
+ /* update volume values */ | |
+ this->current_volume = new_volume; | |
+ this->CalculateAdjustedControllers(); | |
+ | |
+ if (performance == NULL) return; | |
+ | |
+ DEBUG(driver, 2, "DirectMusic: user adjust volume, new adjusted values = %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x", | |
+ this->adjusted_controllers[ 0], this->adjusted_controllers[ 1], this->adjusted_controllers[ 2], this->adjusted_controllers[ 3], | |
+ this->adjusted_controllers[ 4], this->adjusted_controllers[ 5], this->adjusted_controllers[ 6], this->adjusted_controllers[ 7], | |
+ this->adjusted_controllers[ 8], this->adjusted_controllers[ 9], this->adjusted_controllers[10], this->adjusted_controllers[11], | |
+ this->adjusted_controllers[12], this->adjusted_controllers[13], this->adjusted_controllers[14], this->adjusted_controllers[15] | |
+ ); | |
+ | |
+ /* send volume change messages to all channels for instant update */ | |
+ IDirectMusicGraph *graph = NULL; | |
+ if (FAILED(performance->QueryInterface(IID_IDirectMusicGraph, (LPVOID*)&graph))) return; | |
+ | |
+ MUSIC_TIME time = 0; | |
+ performance->GetTime(NULL, &time); | |
+ | |
+ for (int ch = 0; ch < 16; ch++) { | |
+ DMUS_MIDI_PMSG *msg = NULL; | |
+ if (SUCCEEDED(performance->AllocPMsg(sizeof(*msg), (DMUS_PMSG**)&msg))) { | |
+ memset(msg, 0, sizeof(*msg)); | |
+ msg->dwSize = sizeof(*msg); | |
+ msg->dwType = DMUS_PMSGT_MIDI; | |
+ msg->punkUser = this; /* sentinel to indicate this message is to update playback volume, not part of the original song */ | |
+ msg->dwFlags = DMUS_PMSGF_MUSICTIME; | |
+ msg->dwPChannel = ch; /* technically wrong, but DirectMusic doesn't have a way to obtain PChannel number given a MIDI channel, you just have to know it */ | |
+ msg->mtTime = time; | |
+ msg->bStatus = 0xB0 | ch; /* controller change for channel ch */ | |
+ msg->bByte1 = 0x07; /* channel volume controller */ | |
+ msg->bByte2 = this->adjusted_controllers[ch]; | |
+ graph->StampPMsg((DMUS_PMSG*)msg); | |
+ if (FAILED(performance->SendPMsg((DMUS_PMSG*)msg))) { | |
+ performance->FreePMsg((DMUS_PMSG*)msg); | |
+ } | |
+ } | |
+ } | |
+ | |
+ graph->Release(); | |
+ } | |
+}; | |
+/** static instance of the volume control tool */ | |
+VolumeControlTool VolumeControlTool::instance; | |
+ | |
+ | |
const char *MusicDriver_DMusic::Start(const char * const *parm) | |
{ | |
if (performance != NULL) return NULL; | |
@@ -152,6 +297,20 @@ const char *MusicDriver_DMusic::Start(const char * const *parm) | |
return "AddPort failed"; | |
} | |
+ IDirectMusicGraph *graph = NULL; | |
+ if (FAILED(proc.CoCreateInstance( | |
+ CLSID_DirectMusicGraph, | |
+ NULL, | |
+ CLSCTX_INPROC, | |
+ IID_IDirectMusicGraph, | |
+ (LPVOID*)&graph | |
+ ))) { | |
+ return "Failed to create the graph object"; | |
+ } | |
+ graph->InsertTool(&VolumeControlTool::instance, NULL, 0, 0); | |
+ performance->SetGraph(graph); | |
+ graph->Release(); | |
+ | |
/* Assign a performance channel block to the performance if we added | |
* a custom port to the performance. */ | |
if (music_port != NULL) { | |
@@ -188,7 +347,12 @@ void MusicDriver_DMusic::Stop() | |
{ | |
seeking = false; | |
- if (performance != NULL) performance->Stop(NULL, NULL, 0, 0); | |
+ if (performance != NULL) { | |
+ performance->Stop(NULL, NULL, 0, 0); | |
+ /* necessary to sleep, otherwise note-off messages aren't always sent to the output device | |
+ * and can leave notes hanging on external synths, in particular during game shutdown */ | |
+ Sleep(100); | |
+ } | |
if (segment != NULL) { | |
segment->SetParam(GUID_Unload, 0xFFFFFFFF, 0, 0, performance); | |
@@ -216,7 +380,7 @@ void MusicDriver_DMusic::Stop() | |
} | |
-void MusicDriver_DMusic::PlaySong(const char *filename) | |
+void MusicDriver_DMusic::PlaySong(const char *filename, int time_start, int time_end, bool loop) | |
{ | |
/* set up the loader object info */ | |
DMUS_OBJECTDESC obj_desc; | |
@@ -252,6 +416,19 @@ void MusicDriver_DMusic::PlaySong(const char *filename) | |
return; | |
} | |
+ /* All original TTD music has a timedivision of 384, while DirectMusic internally | |
+ * works with a timedivision of 768 i.e. double. Not hardcoding this would require | |
+ * either more addendum data in the baseset files, or reading a couple bytes from | |
+ * the MIDI files to get the actual timedivision, and scale accordingly. */ | |
+ time_start *= 2; | |
+ time_end *= 2; | |
+ segment->SetStartPoint(time_start); | |
+ /* Enable looping if required */ | |
+ if (time_end > time_start && loop) { | |
+ segment->SetLoopPoints(time_start, time_end); | |
+ segment->SetRepeats(DMUS_SEG_REPEAT_INFINITE); | |
+ } | |
+ | |
/* tell the segment to 'download' the instruments */ | |
if (FAILED(segment->SetParam(GUID_Download, 0xFFFFFFFF, 0, 0, performance))) { | |
DEBUG(driver, 0, "DirectMusic: failed to download instruments"); | |
@@ -259,11 +436,18 @@ void MusicDriver_DMusic::PlaySong(const char *filename) | |
} | |
/* start playing the MIDI file */ | |
+ MUSIC_TIME perf_time_start = 0; | |
+ performance->GetTime(NULL, &perf_time_start); | |
if (FAILED(performance->PlaySegment(segment, 0, 0, NULL))) { | |
DEBUG(driver, 0, "DirectMusic: PlaySegment failed"); | |
return; | |
} | |
+ /* If an ending time is given, request a stop at that point */ | |
+ if (time_end > time_start && !loop) { | |
+ performance->Stop(segment, NULL, perf_time_start + time_end, DMUS_SEGF_DEFAULT); | |
+ } | |
+ | |
seeking = true; | |
} | |
@@ -292,8 +476,7 @@ bool MusicDriver_DMusic::IsSongPlaying() | |
void MusicDriver_DMusic::SetVolume(byte vol) | |
{ | |
- long db = vol * 2000 / 127 - 2000; ///< 0 - 127 -> -2000 - 0 | |
- performance->SetGlobalParam(GUID_PerfMasterVolume, &db, sizeof(db)); | |
+ VolumeControlTool::instance.SetVolume(vol); | |
} | |
diff --git a/src/music/dmusic.h b/src/music/dmusic.h | |
index 7287623..8a31eb5 100644 | |
--- a/src/music/dmusic.h | |
+++ b/src/music/dmusic.h | |
@@ -23,7 +23,7 @@ public: | |
/* virtual */ void Stop(); | |
- /* virtual */ void PlaySong(const char *filename); | |
+ /* virtual */ void PlaySong(const char *filename, int time_start = 0, int time_end = 0, bool loop = false); | |
/* virtual */ void StopSong(); | |
diff --git a/src/music/extmidi.cpp b/src/music/extmidi.cpp | |
index d39a050..572e11a 100644 | |
--- a/src/music/extmidi.cpp | |
+++ b/src/music/extmidi.cpp | |
@@ -83,7 +83,7 @@ void MusicDriver_ExtMidi::Stop() | |
this->DoStop(); | |
} | |
-void MusicDriver_ExtMidi::PlaySong(const char *filename) | |
+void MusicDriver_ExtMidi::PlaySong(const char *filename, int time_start, int time_end, bool loop) | |
{ | |
strecpy(this->song, filename, lastof(this->song)); | |
this->DoStop(); | |
diff --git a/src/music/extmidi.h b/src/music/extmidi.h | |
index cfbd894..5724b34 100644 | |
--- a/src/music/extmidi.h | |
+++ b/src/music/extmidi.h | |
@@ -28,7 +28,7 @@ public: | |
/* virtual */ void Stop(); | |
- /* virtual */ void PlaySong(const char *filename); | |
+ /* virtual */ void PlaySong(const char *filename, int time_start = 0, int time_end = 0, bool loop = false); | |
/* virtual */ void StopSong(); | |
diff --git a/src/music/libtimidity.cpp b/src/music/libtimidity.cpp | |
index 1cb2adc..a50991e 100644 | |
--- a/src/music/libtimidity.cpp | |
+++ b/src/music/libtimidity.cpp | |
@@ -96,7 +96,7 @@ void MusicDriver_LibTimidity::Stop() | |
mid_exit(); | |
} | |
-void MusicDriver_LibTimidity::PlaySong(const char *filename) | |
+void MusicDriver_LibTimidity::PlaySong(const char *filename, int time_start, int time_end, bool loop) | |
{ | |
this->StopSong(); | |
diff --git a/src/music/libtimidity.h b/src/music/libtimidity.h | |
index abe17e7..2ee7ecc 100644 | |
--- a/src/music/libtimidity.h | |
+++ b/src/music/libtimidity.h | |
@@ -21,7 +21,7 @@ public: | |
/* virtual */ void Stop(); | |
- /* virtual */ void PlaySong(const char *filename); | |
+ /* virtual */ void PlaySong(const char *filename, int time_start = 0, int time_end = 0, bool loop = false); | |
/* virtual */ void StopSong(); | |
diff --git a/src/music/midifile.cpp b/src/music/midifile.cpp | |
new file mode 100644 | |
index 0000000..4eb9dd2 | |
--- /dev/null | |
+++ b/src/music/midifile.cpp | |
@@ -0,0 +1,403 @@ | |
+/* $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 <http://www.gnu.org/licenses/>. | |
+*/ | |
+ | |
+/* @file midifile.cpp Parser for standard MIDI files */ | |
+ | |
+#include "midifile.hpp" | |
+#include "../fileio_func.h" | |
+#include "../fileio_type.h" | |
+#include <algorithm> | |
+ | |
+ | |
+/* 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<byte>(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<typename T> | |
+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<MidiFile::TempoChange>); | |
+ std::sort(target.blocks.begin(), target.blocks.end(), TicktimeAscending<MidiFile::DataBlock>); | |
+ | |
+ 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<MidiFile::DataBlock> 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; | |
+} | |
+ | |
+/** | |
+ * 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; | |
+ size_t filesize = 0; | |
+ FILE *file = FioFOpenFile(filename, "rb", Subdirectory::BASESET_DIR, &filesize); | |
+ | |
+ /* try to read header, fixed size */ | |
+ byte header[14]; | |
+ if (fread(header, sizeof(header), 1, file) != 1) | |
+ goto cleanup; | |
+ /* 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(header, magic, sizeof(magic)) != 0) | |
+ goto cleanup; | |
+ /* read the parameters of the file */ | |
+ uint16 midi_format = (header[8] << 8) | header[9]; | |
+ uint16 midi_tracks = (header[10] << 8) | header[11]; | |
+ uint16 midi_tickdiv = (header[12] << 8) | header[13]; | |
+ | |
+ /* only format 0 (single-track) and format 1 (multi-track single-song) are accepted for now */ | |
+ if (midi_format != 0 && midi_format != 1) | |
+ goto cleanup; | |
+ /* doesn't support SMPTE timecode files */ | |
+ if ((midi_tickdiv & 0x8000) != 0) | |
+ goto cleanup; | |
+ | |
+ this->tickdiv = midi_tickdiv; | |
+ | |
+ for (; midi_tracks > 0; midi_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..595a7c1 | |
--- /dev/null | |
+++ b/src/music/midifile.hpp | |
@@ -0,0 +1,36 @@ | |
+/* $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 <http://www.gnu.org/licenses/>. | |
+*/ | |
+ | |
+/* @file midifile.hpp Parser for standard MIDI files */ | |
+ | |
+#include "../stdafx.h" | |
+#include "../core/smallvec_type.hpp" | |
+#include <vector> | |
+ | |
+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<byte, 8> 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) { } | |
+ }; | |
+ | |
+ std::vector<DataBlock> blocks; ///< sequential time-annotated data of file, merged to a single track | |
+ std::vector<TempoChange> tempos; ///< list of tempo changes in file | |
+ uint16 tickdiv; ///< ticks per quarter note | |
+ | |
+ bool LoadFile(const char *filename); | |
+ | |
+ void MoveFrom(MidiFile &other); | |
+}; | |
diff --git a/src/music/music_driver.hpp b/src/music/music_driver.hpp | |
index be09d3e..5a0d5f5 100644 | |
--- a/src/music/music_driver.hpp | |
+++ b/src/music/music_driver.hpp | |
@@ -21,7 +21,7 @@ public: | |
* Play a particular song. | |
* @param filename The name of file with the song to play. | |
*/ | |
- virtual void PlaySong(const char *filename) = 0; | |
+ virtual void PlaySong(const char *filename, int time_start = 0, int time_end = 0, bool loop = false) = 0; | |
/** | |
* Stop playing the current song. | |
diff --git a/src/music/null_m.h b/src/music/null_m.h | |
index df9f7d8..0e615bd 100644 | |
--- a/src/music/null_m.h | |
+++ b/src/music/null_m.h | |
@@ -21,7 +21,7 @@ public: | |
/* virtual */ void Stop() { } | |
- /* virtual */ void PlaySong(const char *filename) { } | |
+ /* virtual */ void PlaySong(const char *filename, int time_start = 0, int time_end = 0, bool loop = false) { } | |
/* virtual */ void StopSong() { } | |
diff --git a/src/music/os2_m.cpp b/src/music/os2_m.cpp | |
index d7fb97d..622008e 100644 | |
--- a/src/music/os2_m.cpp | |
+++ b/src/music/os2_m.cpp | |
@@ -49,7 +49,7 @@ static long CDECL MidiSendCommand(const char *cmd, ...) | |
/** OS/2's music player's factory. */ | |
static FMusicDriver_OS2 iFMusicDriver_OS2; | |
-void MusicDriver_OS2::PlaySong(const char *filename) | |
+void MusicDriver_OS2::PlaySong(const char *filename, int time_start, int time_end, bool loop) | |
{ | |
MidiSendCommand("close all"); | |
diff --git a/src/music/os2_m.h b/src/music/os2_m.h | |
index f35e2fd..f01b0bf 100644 | |
--- a/src/music/os2_m.h | |
+++ b/src/music/os2_m.h | |
@@ -21,7 +21,7 @@ public: | |
/* virtual */ void Stop(); | |
- /* virtual */ void PlaySong(const char *filename); | |
+ /* virtual */ void PlaySong(const char *filename, int time_start = 0, int time_end = 0, bool loop = false); | |
/* virtual */ void StopSong(); | |
diff --git a/src/music/qtmidi.cpp b/src/music/qtmidi.cpp | |
index 9bc6a61..4219695 100644 | |
--- a/src/music/qtmidi.cpp | |
+++ b/src/music/qtmidi.cpp | |
@@ -258,7 +258,7 @@ void MusicDriver_QtMidi::Stop() | |
* | |
* @param filename Path to a MIDI file. | |
*/ | |
-void MusicDriver_QtMidi::PlaySong(const char *filename) | |
+void MusicDriver_QtMidi::PlaySong(const char *filename, int time_start, int time_end, bool loop) | |
{ | |
if (!_quicktime_started) return; | |
diff --git a/src/music/qtmidi.h b/src/music/qtmidi.h | |
index f0e1708..4b29324 100644 | |
--- a/src/music/qtmidi.h | |
+++ b/src/music/qtmidi.h | |
@@ -20,7 +20,7 @@ public: | |
/* virtual */ void Stop(); | |
- /* virtual */ void PlaySong(const char *filename); | |
+ /* virtual */ void PlaySong(const char *filename, int time_start = 0, int time_end = 0, bool loop = false); | |
/* virtual */ void StopSong(); | |
diff --git a/src/music/win32_m.cpp b/src/music/win32_m.cpp | |
index fff0376..09ba71a 100644 | |
--- a/src/music/win32_m.cpp | |
+++ b/src/music/win32_m.cpp | |
@@ -15,170 +15,352 @@ | |
#include <windows.h> | |
#include <mmsystem.h> | |
#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) | |
-{ | |
- 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<MIDIHDR>(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<byte>(_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; | |
- 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; | |
+ DEBUG(driver, 2, "Win32-MIDI: PlaySong: setting flag"); | |
+ _midi.do_start = true; | |
+ | |
+ 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, 0, "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); | |
} | |
diff --git a/src/music/win32_m.h b/src/music/win32_m.h | |
index 3efee32..cec1b1c 100644 | |
--- a/src/music/win32_m.h | |
+++ b/src/music/win32_m.h | |
@@ -21,7 +21,7 @@ public: | |
/* virtual */ void Stop(); | |
- /* virtual */ void PlaySong(const char *filename); | |
+ /* virtual */ void PlaySong(const char *filename, int time_start = 0, int time_end = 0, bool loop = false); | |
/* virtual */ void StopSong(); | |
diff --git a/src/music_gui.cpp b/src/music_gui.cpp | |
index 279f376..f165867 100644 | |
--- a/src/music_gui.cpp | |
+++ b/src/music_gui.cpp | |
@@ -109,6 +109,7 @@ void InitializeMusic() | |
uint j = 0; | |
for (uint i = 0; i < NUM_SONGS_AVAILABLE; i++) { | |
if (StrEmpty(GetSongName(i))) continue; | |
+ if (i == 0 && BaseMusic::GetUsedSet()->has_theme) continue; | |
_playlist_all[j++] = i + 1; | |
} | |
/* Terminate the list */ | |
@@ -174,18 +175,27 @@ static void SkipToNextSong() | |
_song_is_active = false; | |
} | |
+byte _next_update_music_volume = 0; | |
+byte _next_music_volume = 127; | |
static void MusicVolumeChanged(byte new_vol) | |
{ | |
- MusicDriver::GetInstance()->SetVolume(new_vol); | |
+ _next_update_music_volume = 2; | |
+ _next_music_volume = new_vol; | |
} | |
static void DoPlaySong() | |
{ | |
char filename[MAX_PATH]; | |
- if (FioFindFullPath(filename, lastof(filename), BASESET_DIR, BaseMusic::GetUsedSet()->files[_music_wnd_cursong - 1].filename) == NULL) { | |
- FioFindFullPath(filename, lastof(filename), OLD_GM_DIR, BaseMusic::GetUsedSet()->files[_music_wnd_cursong - 1].filename); | |
+ int songid = _music_wnd_cursong - 1; | |
+ const MusicSet &set = *BaseMusic::GetUsedSet(); | |
+ if (FioFindFullPath(filename, lastof(filename), BASESET_DIR, set.files[songid].filename) == NULL) { | |
+ FioFindFullPath(filename, lastof(filename), OLD_GM_DIR, set.files[songid].filename); | |
} | |
- MusicDriver::GetInstance()->PlaySong(filename); | |
+ bool loop = false; | |
+ if (_music_wnd_cursong == 1 && _game_mode == GM_MENU && set.has_theme) { | |
+ loop = true; | |
+ } | |
+ MusicDriver::GetInstance()->PlaySong(filename, set.override_start[songid], set.override_end[songid], loop); | |
SetWindowDirty(WC_MUSIC_WINDOW, 0); | |
} | |
@@ -243,6 +253,16 @@ static void StopMusic() | |
static void PlayPlaylistSong() | |
{ | |
+ if (_game_mode == GM_MENU && BaseMusic::GetUsedSet()->has_theme) { | |
+ /* force first song (theme) on the main menu. | |
+ * this is guaranteed to exist, otherwise | |
+ * has_theme would not be set. */ | |
+ _music_wnd_cursong = 1; | |
+ DoPlaySong(); | |
+ _song_is_active = true; | |
+ return; | |
+ } | |
+ | |
if (_cur_playlist[0] == 0) { | |
SelectSongToPlay(); | |
/* if there is not songs in the playlist, it may indicate | |
@@ -268,8 +288,26 @@ void ResetMusic() | |
DoPlaySong(); | |
} | |
+/** | |
+ * Check music playback status and start/stop/song-finished | |
+ * Called from main loop. | |
+ */ | |
void MusicLoop() | |
{ | |
+ static GameMode _last_game_mode = GM_BOOTSTRAP; | |
+ bool force_restart = false; | |
+ if (_game_mode != _last_game_mode) { | |
+ force_restart = true; | |
+ _last_game_mode = _game_mode; | |
+ } | |
+ | |
+ if (_next_update_music_volume > 0) { | |
+ _next_update_music_volume--; | |
+ if (_next_update_music_volume == 0) { | |
+ MusicDriver::GetInstance()->SetVolume(_next_music_volume); | |
+ } | |
+ } | |
+ | |
if (!_settings_client.music.playing && _song_is_active) { | |
StopMusic(); | |
} else if (_settings_client.music.playing && !_song_is_active) { | |
@@ -278,13 +316,13 @@ void MusicLoop() | |
if (!_song_is_active) return; | |
- if (!MusicDriver::GetInstance()->IsSongPlaying()) { | |
- if (_game_mode != GM_MENU) { | |
+ if (force_restart || !MusicDriver::GetInstance()->IsSongPlaying()) { | |
+ if (_game_mode == GM_MENU && BaseMusic::GetUsedSet()->has_theme) { | |
+ ResetMusic(); | |
+ } else { | |
StopMusic(); | |
SkipToNextSong(); | |
PlayPlaylistSong(); | |
- } else { | |
- ResetMusic(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment