Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Music playback patches for OpenTTD r27967
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
You can’t perform that action at this time.