summaryrefslogtreecommitdiff
path: root/src/music
diff options
context:
space:
mode:
Diffstat (limited to 'src/music')
-rw-r--r--src/music/midi.h144
-rw-r--r--src/music/midifile.cpp457
-rw-r--r--src/music/midifile.hpp44
-rw-r--r--src/music/win32_m.cpp432
4 files changed, 966 insertions, 111 deletions
diff --git a/src/music/midi.h b/src/music/midi.h
new file mode 100644
index 000000000..473f7f18b
--- /dev/null
+++ b/src/music/midi.h
@@ -0,0 +1,144 @@
+/* $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 midi.h Declarations for MIDI data */
+
+#ifndef MUSIC_MIDI_H
+#define MUSIC_MIDI_H
+
+#include "../stdafx.h"
+
+/** Header of a Stanard MIDI File */
+struct SMFHeader {
+ uint16 format;
+ uint16 tracks;
+ uint16 tickdiv;
+};
+
+/** MIDI status byte codes */
+enum MidiStatus {
+ /* Bytes with top bit unset are data bytes i.e. not status bytes */
+ /* Channel status messages, require channel number in lower nibble */
+ MIDIST_NOTEOFF = 0x80,
+ MIDIST_NOTEON = 0x90,
+ MIDIST_POLYPRESS = 0xA0,
+ MIDIST_CONTROLLER = 0xB0,
+ MIDIST_PROGCHG = 0xC0,
+ MIDIST_CHANPRESS = 0xD0,
+ MIDIST_PITCHBEND = 0xE0,
+ /* Full byte status messages */
+ MIDIST_SYSEX = 0xF0,
+ MIDIST_TC_QFRAME = 0xF1,
+ MIDIST_SONGPOSPTR = 0xF2,
+ MIDIST_SONGSEL = 0xF3,
+ /* not defined: 0xF4, */
+ /* not defined: 0xF5, */
+ MIDIST_TUNEREQ = 0xF6,
+ MIDIST_ENDSYSEX = 0xF7, ///< only occurs in realtime data
+ MIDIST_SMF_ESCAPE = 0xF7, ///< only occurs in SMF data
+ MIDIST_RT_CLOCK = 0xF8,
+ /* not defined: 0xF9, */
+ MIDIST_RT_START = 0xFA,
+ MIDIST_RT_CONTINUE = 0xFB,
+ MIDIST_RT_STOP = 0xFC,
+ /* not defined: 0xFD, */
+ MIDIST_RT_ACTSENS = 0xFE,
+ MIDIST_SYSRESET = 0xFF, ///< only occurs in realtime data
+ MIDIST_SMF_META = 0xFF, ///< only occurs in SMF data
+};
+
+/**
+ * MIDI controller numbers.
+ * Complete list per General MIDI, missing values are not defined.
+ */
+enum MidiController {
+ /* Standard continuous controllers (MSB control) */
+ MIDICT_BANKSELECT = 0,
+ MIDICT_MODWHEEL = 1,
+ MIDICT_BREATH = 2,
+ MIDICT_FOOT = 4,
+ MIDICT_PORTAMENTO = 5,
+ MIDICT_DATAENTRY = 6,
+ MIDICT_CHANVOLUME = 7,
+ MIDICT_BALANCE = 8,
+ MIDICT_PAN = 10,
+ MIDICT_EXPRESSION = 11,
+ MIDICT_EFFECT1 = 12,
+ MIDICT_EFFECT2 = 13,
+ MIDICT_GENERAL1 = 16,
+ MIDICT_GENERAL2 = 17,
+ MIDICT_GENERAL3 = 18,
+ MIDICT_GENERAL4 = 19,
+ /* Offset from MSB to LSB of continuous controllers */
+ MIDICTOFS_HIGHRES = 32,
+ /* Stanard continuous controllers (LSB control) */
+ MIDICT_BANKSELECT_LO = MIDICTOFS_HIGHRES + MIDICT_BANKSELECT,
+ MIDICT_MODWHEEL_LO = MIDICTOFS_HIGHRES + MIDICT_MODWHEEL,
+ MIDICT_BREATH_LO = MIDICTOFS_HIGHRES + MIDICT_BREATH,
+ MIDICT_FOOT_LO = MIDICTOFS_HIGHRES + MIDICT_FOOT,
+ MIDICT_PORTAMENTO_LO = MIDICTOFS_HIGHRES + MIDICT_PORTAMENTO,
+ MIDICT_DATAENTRY_LO = MIDICTOFS_HIGHRES + MIDICT_DATAENTRY,
+ MIDICT_CHANVOLUME_LO = MIDICTOFS_HIGHRES + MIDICT_CHANVOLUME,
+ MIDICT_BALANCE_LO = MIDICTOFS_HIGHRES + MIDICT_BALANCE,
+ MIDICT_PAN_LO = MIDICTOFS_HIGHRES + MIDICT_PAN,
+ MIDICT_EXPRESSION_LO = MIDICTOFS_HIGHRES + MIDICT_EXPRESSION,
+ MIDICT_EFFECT1_LO = MIDICTOFS_HIGHRES + MIDICT_EFFECT1,
+ MIDICT_EFFECT2_LO = MIDICTOFS_HIGHRES + MIDICT_EFFECT2,
+ MIDICT_GENERAL1_LO = MIDICTOFS_HIGHRES + MIDICT_GENERAL1,
+ MIDICT_GENERAL2_LO = MIDICTOFS_HIGHRES + MIDICT_GENERAL2,
+ MIDICT_GENERAL3_LO = MIDICTOFS_HIGHRES + MIDICT_GENERAL3,
+ MIDICT_GENERAL4_LO = MIDICTOFS_HIGHRES + MIDICT_GENERAL4,
+ /* Switch controllers */
+ MIDICT_SUSTAINSW = 64,
+ MIDICT_PORTAMENTOSW = 65,
+ MIDICT_SOSTENUTOSW = 66,
+ MIDICT_SOFTPEDALSW = 67,
+ MIDICT_LEGATOSW = 68,
+ MIDICT_HOLD2SW = 69,
+ /* Standard low-resolution controllers */
+ MIDICT_SOUND1 = 70,
+ MIDICT_SOUND2 = 71,
+ MIDICT_SOUND3 = 72,
+ MIDICT_SOUND4 = 73,
+ MIDICT_SOUND5 = 74,
+ MIDICT_SOUND6 = 75,
+ MIDICT_SOUND7 = 76,
+ MIDICT_SOUND8 = 77,
+ MIDICT_SOUND9 = 78,
+ MIDICT_SOUND10 = 79,
+ MIDICT_GENERAL5 = 80,
+ MIDICT_GENERAL6 = 81,
+ MIDICT_GENERAL7 = 82,
+ MIDICT_GENERAL8 = 83,
+ MIDICT_PORTAMENTOCTL = 84,
+ MIDICT_PRF_HIGHRESVEL = 88,
+ MIDICT_EFFECTS1 = 91,
+ MIDICT_EFFECTS2 = 92,
+ MIDICT_EFFECTS3 = 93,
+ MIDICT_EFFECTS4 = 94,
+ MIDICT_EFFECTS5 = 95,
+ /* Registered/unregistered parameters control */
+ MIDICT_DATA_INCREMENT = 96,
+ MIDICT_DATA_DECREMENT = 97,
+ MIDICT_NRPN_SELECT_LO = 98,
+ MIDICT_NRPN_SELECT_HI = 99,
+ MIDICT_RPN_SELECT_LO = 100,
+ MIDICT_RPN_SELECT_HI = 101,
+ /* Channel mode messages */
+ MIDICT_MODE_ALLSOUNDOFF = 120,
+ MIDICT_MODE_RESETALLCTRL = 121,
+ MIDICT_MODE_LOCALCTL = 122,
+ MIDICT_MODE_ALLNOTESOFF = 123,
+ MIDICT_MODE_OMNI_OFF = 124,
+ MIDICT_MODE_OMNI_ON = 125,
+ MIDICT_MODE_MONO = 126,
+ MIDICT_MODE_POLY = 127,
+};
+
+#endif /* MUSIC_MIDI_H */
diff --git a/src/music/midifile.cpp b/src/music/midifile.cpp
new file mode 100644
index 000000000..eb7e02303
--- /dev/null
+++ b/src/music/midifile.cpp
@@ -0,0 +1,457 @@
+/* $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 "../core/endian_func.hpp"
+#include "midi.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 */
+ uint32 chunk_length;
+ if (fread(&chunk_length, 1, 4, file) != 4) {
+ return false;
+ }
+ chunk_length = FROM_BE32(chunk_length);
+
+ 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 MIDIST_NOTEOFF:
+ case MIDIST_NOTEON:
+ case MIDIST_POLYPRESS:
+ case MIDIST_CONTROLLER:
+ case MIDIST_PITCHBEND:
+ /* 3 byte messages */
+ data = block->data.Append(3);
+ data[0] = status;
+ if (!chunk.ReadBuffer(&data[1], 2)) {
+ return false;
+ }
+ break;
+ case MIDIST_PROGCHG:
+ case MIDIST_CHANPRESS:
+ /* 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 == MIDIST_SMF_META) {
+ /* 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 (length != 0 is illegal) */
+ return (length == 0);
+ 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 == MIDIST_SYSEX || (status == MIDIST_SMF_ESCAPE && 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 == MIDIST_SMF_ESCAPE) {
+ /* 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;
+ size_t cur_tempo = 0, cur_block = 0;
+ while (cur_block < target.blocks.size()) {
+ MidiFile::DataBlock &block = target.blocks[cur_block];
+ MidiFile::TempoChange &tempo = target.tempos[cur_tempo];
+ MidiFile::TempoChange &next_tempo = target.tempos[cur_tempo+1];
+ if (block.ticktime <= next_tempo.ticktime) {
+ /* block is within the current tempo */
+ int64 tickdiff = block.ticktime - last_ticktime;
+ last_ticktime = block.ticktime;
+ last_realtime += uint32(tickdiff * tempo.tempo / target.tickdiv);
+ block.realtime = last_realtime;
+ cur_block++;
+ } else {
+ /* tempo change occurs before this block */
+ int64 tickdiff = next_tempo.ticktime - last_ticktime;
+ last_ticktime = next_tempo.ticktime;
+ last_realtime += uint32(tickdiff * tempo.tempo / target.tickdiv); // current tempo until the tempo change
+ cur_tempo++;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Read the header of a standard MIDI file.
+ * @param[in] filename name of file to read from
+ * @param[out] header filled with data read
+ * @return true if the file could be opened and contained a header with correct format
+ */
+bool MidiFile::ReadSMFHeader(const char *filename, SMFHeader &header)
+{
+ FILE *file = FioFOpenFile(filename, "rb", Subdirectory::BASESET_DIR);
+ if (!file) return false;
+ bool result = ReadSMFHeader(file, header);
+ FioFCloseFile(file);
+ return result;
+}
+
+/**
+ * Read the header of a standard MIDI file.
+ * The function will consume 14 bytes from the current file pointer position.
+ * @param[in] file open file to read from (should be in binary mode)
+ * @param[out] header filled with data read
+ * @return true if a header in correct format could be read from the file
+ */
+bool MidiFile::ReadSMFHeader(FILE *file, SMFHeader &header)
+{
+ /* Try to read header, fixed size */
+ byte buffer[14];
+ if (fread(buffer, sizeof(buffer), 1, file) != 1) {
+ return false;
+ }
+
+ /* check magic, 'MThd' followed by 4 byte length indicator (always = 6 in SMF) */
+ const byte magic[] = { 'M', 'T', 'h', 'd', 0x00, 0x00, 0x00, 0x06 };
+ if (MemCmpT(buffer, magic, sizeof(magic)) != 0) {
+ return false;
+ }
+
+ /* read the parameters of the file */
+ header.format = (buffer[8] << 8) | buffer[9];
+ header.tracks = (buffer[10] << 8) | buffer[11];
+ header.tickdiv = (buffer[12] << 8) | buffer[13];
+ return true;
+}
+
+/**
+ * Load a standard MIDI file.
+ * @param filename name of the file to load
+ * @returns true if loaded was successful
+ */
+bool MidiFile::LoadFile(const char *filename)
+{
+ this->blocks.clear();
+ this->tempos.clear();
+ this->tickdiv = 0;
+
+ bool success = false;
+ FILE *file = FioFOpenFile(filename, "rb", Subdirectory::BASESET_DIR);
+
+ SMFHeader header;
+ if (!ReadSMFHeader(file, header)) goto cleanup;
+
+ /* Only format 0 (single-track) and format 1 (multi-track single-song) are accepted for now */
+ if (header.format != 0 && header.format != 1) goto cleanup;
+ /* Doesn't support SMPTE timecode files */
+ if ((header.tickdiv & 0x8000) != 0) goto cleanup;
+
+ this->tickdiv = header.tickdiv;
+
+ for (; header.tracks > 0; header.tracks--) {
+ if (!ReadTrackChunk(file, *this)) {
+ goto cleanup;
+ }
+ }
+
+ success = FixupMidiData(*this);
+
+cleanup:
+ FioFCloseFile(file);
+ return success;
+}
+
+/**
+ * Move data from other to this, and clears other.
+ * @param other object containing loaded data to take over
+ */
+void MidiFile::MoveFrom(MidiFile &other)
+{
+ std::swap(this->blocks, other.blocks);
+ std::swap(this->tempos, other.tempos);
+ this->tickdiv = other.tickdiv;
+
+ other.blocks.clear();
+ other.tempos.clear();
+ other.tickdiv = 0;
+}
+
diff --git a/src/music/midifile.hpp b/src/music/midifile.hpp
new file mode 100644
index 000000000..d077f63cd
--- /dev/null
+++ b/src/music/midifile.hpp
@@ -0,0 +1,44 @@
+/* $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 */
+
+#ifndef MUSIC_MIDIFILE_HPP
+#define MUSIC_MIDIFILE_HPP
+
+#include "../stdafx.h"
+#include "../core/smallvec_type.hpp"
+#include "midi.h"
+#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);
+
+ static bool ReadSMFHeader(const char *filename, SMFHeader &header);
+ static bool ReadSMFHeader(FILE *file, SMFHeader &header);
+};
+
+#endif /* MUSIC_MIDIFILE_HPP */
diff --git a/src/music/win32_m.cpp b/src/music/win32_m.cpp
index fff0376a0..57bbd1f93 100644
--- a/src/music/win32_m.cpp
+++ b/src/music/win32_m.cpp
@@ -15,170 +15,380 @@
#include <windows.h>
#include <mmsystem.h>
#include "../os/windows/win32.h"
+#include "../debug.h"
+#include "midifile.hpp"
+#include "midi.h"
#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; ///< obtained timer precision value
+ HMIDIOUT midi_out; ///< handle to open midiOut
+ UINT timer_id; ///< ID of active multimedia timer
+ CRITICAL_SECTION lock; ///< synchronization for playback status fields
+
+ 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 began playback
+ 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]; ///< last seen volume controller values in raw data
} _midi;
static FMusicDriver_Win32 iFMusicDriver_Win32;
-void MusicDriver_Win32::PlaySong(const char *filename)
+
+static byte ScaleVolume(byte original, byte scale)
{
- assert(filename != NULL);
- strecpy(_midi.start_song, filename, lastof(_midi.start_song));
- _midi.playing = true;
- _midi.stop_song = false;
- SetEvent(_midi.wait_obj);
+ return original * scale / 127;
}
-void MusicDriver_Win32::StopSong()
+
+void CALLBACK MidiOutProc(HMIDIOUT hmo, UINT wMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2)
{
- if (_midi.playing) {
- _midi.stop_song = true;
- _midi.start_song[0] = '\0';
- SetEvent(_midi.wait_obj);
+ if (wMsg == MOM_DONE) {
+ MIDIHDR *hdr = (LPMIDIHDR)dwParam1;
+ midiOutUnprepareHeader(hmo, hdr, sizeof(*hdr));
+ free(hdr);
}
}
-bool MusicDriver_Win32::IsSongPlaying()
+static void TransmitChannelMsg(byte status, byte p1, byte p2 = 0)
{
- return _midi.playing;
+ midiOutShortMsg(_midi.midi_out, status | (p1 << 8) | (p2 << 16));
}
-void MusicDriver_Win32::SetVolume(byte vol)
+static void TransmitSysex(byte *&msg_start, size_t &remaining)
{
- _midi.new_vol = vol;
- SetEvent(_midi.wait_obj);
+ /* find end of message */
+ byte *msg_end = msg_start;
+ while (*msg_end != MIDIST_ENDSYSEX) msg_end++;
+ msg_end++; /* also include sysex end byte */
+
+ /* prepare header */
+ MIDIHDR *hdr = CallocT<MIDIHDR>(1);
+ hdr->lpData = (LPSTR)msg_start;
+ hdr->dwBufferLength = msg_end - msg_start;
+ if (midiOutPrepareHeader(_midi.midi_out, hdr, sizeof(*hdr)) == MMSYSERR_NOERROR) {
+ /* transmit - just point directly into the data buffer */
+ 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, ...)
+static void TransmitSysexConst(byte *msg_start, size_t length)
{
- va_list va;
- TCHAR buf[512];
-
- va_start(va, cmd);
- _vsntprintf(buf, lengthof(buf), cmd, va);
- va_end(va);
- return mciSendString(buf, NULL, 0, 0);
+ TransmitSysex(msg_start, length);
}
-static bool MidiIntPlaySong(const char *filename)
+/**
+ * Realtime MIDI playback service routine.
+ * This is called by the multimedia timer.
+ */
+void CALLBACK TimerCallback(UINT uTimerID, UINT, DWORD_PTR dwUser, DWORD_PTR, DWORD_PTR)
{
- MidiSendCommand(_T("close all"));
+ /* Try to check playback status changes.
+ * If _midi is already locked, skip checking for this cycle and try again
+ * next cycle, instead of waiting for locks in the realtime callback. */
+ 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;
+ }
+
+ /* 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(MIDIST_CONTROLLER | ch, MIDICT_CHANVOLUME, vol);
+ }
+ }
+ else {
+ volume_throttle--;
+ }
+ }
+
+ LeaveCriticalSection(&_midi.lock);
+ }
+
+ /* skip beginning of file? */
+ 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 MIDIST_PROGCHG:
+ case MIDIST_CHANPRESS:
+ /* 2 byte channel messages */
+ TransmitChannelMsg(status, data[0]);
+ data++;
+ remaining--;
+ break;
+ case MIDIST_NOTEOFF:
+ case MIDIST_NOTEON:
+ case MIDIST_POLYPRESS:
+ case MIDIST_PITCHBEND:
+ /* 3 byte channel messages */
+ TransmitChannelMsg(status, data[0], data[1]);
+ data += 2;
+ remaining -= 2;
+ break;
+ case MIDIST_CONTROLLER:
+ /* controller change */
+ if (data[0] == MIDICT_CHANVOLUME) {
+ /* 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 MIDIST_SYSEX: /* system exclusive */
+ TransmitSysex(data, remaining);
+ break;
+ case MIDIST_TC_QFRAME: /* time code quarter frame */
+ case MIDIST_SONGSEL: /* song select */
+ data++;
+ remaining--;
+ break;
+ case MIDIST_SONGPOSPTR: /* song position pointer */
+ data += 2;
+ remaining -= 2;
+ break;
+ default: /* remaining have no data bytes */
+ break;
+ }
+ break;
+ }
+ }
- 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;
+ _midi.current_block++;
}
- MidiSendCommand(_T("seek song to start wait"));
- return MidiSendCommand(_T("play song")) == 0;
+ /* end? */
+ 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 void MidiIntStopSong()
+void MusicDriver_Win32::PlaySong(const char *filename)
{
- MidiSendCommand(_T("close all"));
+ DEBUG(driver, 2, "Win32-MIDI: PlaySong: entry");
+ EnterCriticalSection(&_midi.lock);
+
+ _midi.next_file.LoadFile(filename);
+ _midi.next_segment.start = 0;
+ _midi.next_segment.end = 0;
+ _midi.next_segment.loop = false;
+
+ DEBUG(driver, 2, "Win32-MIDI: PlaySong: setting flag");
+ _midi.do_stop = _midi.playing;
+ _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);
+ }
+
+ LeaveCriticalSection(&_midi.lock);
}
-static void MidiIntSetVolume(int vol)
+void MusicDriver_Win32::StopSong()
{
- DWORD v = (vol * 65535 / 127);
- midiOutSetVolume((HMIDIOUT)_midi.devid, v + (v << 16));
+ 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 bool MidiIntIsSongPlaying()
+bool MusicDriver_Win32::IsSongPlaying()
{
- char buf[16];
- mciSendStringA("status song mode", buf, sizeof(buf), 0);
- return strcmp(buf, "playing") == 0 || strcmp(buf, "seeking") == 0;
+ return _midi.playing || _midi.do_start;
}
-static DWORD WINAPI MidiThread(LPVOID arg)
+void MusicDriver_Win32::SetVolume(byte vol)
{
- SetWin32ThreadName(-1, "ottd:win-midi");
+ EnterCriticalSection(&_midi.lock);
+ _midi.new_volume = vol;
+ LeaveCriticalSection(&_midi.lock);
+}
- do {
- char *s;
- int vol;
+const char *MusicDriver_Win32::Start(const char * const *parm)
+{
+ DEBUG(driver, 2, "Win32-MIDI: Start: initializing");
- vol = _midi.new_vol;
- if (vol != -1) {
- _midi.new_vol = -1;
- MidiIntSetVolume(vol);
- }
+ InitializeCriticalSection(&_midi.lock);
- s = _midi.start_song;
- if (s[0] != '\0') {
- _midi.playing = MidiIntPlaySong(s);
- s[0] = '\0';
+ int resolution = GetDriverParamInt(parm, "resolution", 5);
+ int port = GetDriverParamInt(parm, "port", -1);
- /* Delay somewhat in case we don't manage to play. */
- if (!_midi.playing) WaitForMultipleObjects(1, &_midi.wait_obj, FALSE, 5000);
- }
+ UINT devid;
+ if (port < 0) {
+ devid = MIDI_MAPPER;
+ } else {
+ devid = (UINT)port;
+ }
- if (_midi.stop_song && _midi.playing) {
- _midi.stop_song = false;
- _midi.playing = false;
- MidiIntStopSong();
- }
+ resolution = Clamp(resolution, 1, 20);
- if (_midi.playing && !MidiIntIsSongPlaying()) _midi.playing = false;
+ if (midiOutOpen(&_midi.midi_out, devid, (DWORD_PTR)&MidiOutProc, (DWORD_PTR)this, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) {
+ return "could not open midi device";
+ }
- WaitForMultipleObjects(1, &_midi.wait_obj, FALSE, 1000);
- } while (!_midi.terminate);
+ midiOutReset(_midi.midi_out);
- MidiIntStopSong();
- return 0;
-}
+ /* Standard "Enable General MIDI" message */
+ static byte gm_enable_sysex[] = { 0xF0, 0x7E, 0x00, 0x09, 0x01, 0xF7 };
+ TransmitSysexConst(&gm_enable_sysex[0], sizeof(gm_enable_sysex));
-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;
+ /* prepare multimedia timer */
+ 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) {
+ /* success */
+ DEBUG(driver, 2, "Win32-MIDI: Start: timer resolution is %d", (int)_midi.time_period);
+ return NULL;
}
}
-
- if (NULL == (_midi.wait_obj = CreateEvent(NULL, FALSE, FALSE, NULL))) return "Failed to create event";
-
- /* The lpThreadId parameter of CreateThread (the last parameter)
- * may NOT be NULL on Windows 95, 98 and ME. */
- DWORD threadId;
- if (NULL == (_midi.thread = CreateThread(NULL, 8192, MidiThread, 0, 0, &threadId))) return "Failed to create thread";
-
- return NULL;
+ midiOutClose(_midi.midi_out);
+ return "could not set timer resolution";
}
void MusicDriver_Win32::Stop()
{
- _midi.terminate = true;
- SetEvent(_midi.wait_obj);
- WaitForMultipleObjects(1, &_midi.thread, true, INFINITE);
- CloseHandle(_midi.wait_obj);
- CloseHandle(_midi.thread);
+ EnterCriticalSection(&_midi.lock);
+
+ if (_midi.timer_id) {
+ timeKillEvent(_midi.timer_id);
+ _midi.timer_id = 0;
+ }
+
+ timeEndPeriod(_midi.time_period);
+ midiOutReset(_midi.midi_out);
+ midiOutClose(_midi.midi_out);
+
+ LeaveCriticalSection(&_midi.lock);
+ DeleteCriticalSection(&_midi.lock);
}