-
-
Save nielsmh/b0ebe5338a86d0dd0e7b6fdbabd9e230 to your computer and use it in GitHub Desktop.
OTTD new music code
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
/* $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 "../string_func.h" | |
#include "../core/endian_func.hpp" | |
#include <algorithm> | |
/* SMF reader based on description at: http://www.somascape.org/midi/tech/mfile.html */ | |
MidiFile *_midifile_instance = NULL; | |
enum MidiStatus { | |
// These take a channel number in the lower nibble | |
MIDIST_NOTEOFF = 0x80, | |
MIDIST_NOTEON = 0x90, | |
MIDIST_POLYPRESS = 0xA0, | |
MIDIST_CONTROLLER = 0xB0, | |
MIDIST_PROGCHG = 0xC0, | |
MIDIST_CHANPRESS = 0xD0, | |
MIDIST_PITCHBEND = 0xE0, | |
// These are full byte status codes | |
MIDIST_SYSEX = 0xF0, | |
MIDIST_ENDSYSEX = 0xF7, ///< only occurs in realtime data | |
MIDIST_SMF_ESCAPE = 0xF7, ///< only occurs in SMF data | |
MIDIST_TC_QFRAME = 0xF1, | |
MIDIST_SONGPOSPTR = 0xF2, | |
MIDIST_SONGSEL = 0xF3, | |
MIDIST_TUNEREQ = 0xF6, | |
MIDIST_RT_CLOCK = 0xF8, | |
MIDIST_SYSRESET = 0xFF, ///< only occurs in realtime data | |
MIDIST_SMF_META = 0xFF, ///< only occurs in SMF data | |
}; | |
enum MidiController { | |
// Controller number for MSB of high-res controllers | |
MIDICT_BANKSELECT = 0, | |
MIDICT_MODWHEEL, | |
MIDICT_BREATH, | |
MIDICT_FOOT, | |
MIDICT_PORTAMENTO, | |
MIDICT_DATAENTRY, | |
MIDICT_CHANVOLUME, | |
MIDICT_BALANCE, | |
MIDICT_PAN, | |
MIDICT_EXPRESSION, | |
MIDICT_EFFECT1, | |
MIDICT_EFFECT2, | |
MIDICT_GENERAL1, | |
MIDICT_GENERAL2, | |
MIDICT_GENERAL3, | |
MIDICT_GENERAL4, | |
// Offset to add to MSB controller number to get LSB controller number | |
MIDICTOFS_HIGHRES = 32, | |
// Switch controllers | |
MIDICT_SUSTAINSW = 64, | |
MIDICT_PORTAMENTOSW, | |
MIDICT_SOSTENUTOSW, | |
MIDICT_SOFTPEDALSW, | |
MIDICT_LEGATOSW, | |
MIDICT_HOLD2SW, | |
// Channel mode messages | |
MIDICT_MODE_ALLSOUNDOFF = 120, | |
MIDICT_MODE_RESETALLCTRL, | |
MIDICT_MODE_LOCALCTL, | |
MIDICT_MODE_ALLNOTESOFF, | |
MIDICT_MODE_OMNI_OFF, | |
MIDICT_MODE_OMNI_ON, | |
MIDICT_MODE_MONO, | |
MIDICT_MODE_POLY, | |
}; | |
/** | |
* 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); | |
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 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 */ | |
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 == 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.data.Length() == 0) { | |
continue; | |
} else if (block.ticktime > last_ticktime || merged_blocks.size() == 0) { | |
merged_blocks.push_back(block); | |
last_ticktime = block.ticktime; | |
} else { | |
byte *datadest = merged_blocks.back().data.Append(block.data.Length()); | |
memcpy(datadest, block.data.Begin(), block.data.Length()); | |
} | |
} | |
std::swap(merged_blocks, target.blocks); | |
/* Annotate blocks with real time */ | |
last_ticktime = 0; | |
uint32 last_realtime = 0; | |
MidiFile::TempoChange *current_tempo = &target.tempos[0]; | |
for (size_t i = 0; i < target.blocks.size(); i++) { | |
MidiFile::DataBlock &block = target.blocks[i]; | |
uint32 deltaticks = block.ticktime - last_ticktime; | |
/* Check if deltatime gets sliced by up tempo change */ | |
if (block.ticktime <= current_tempo[1].ticktime) { | |
/* Simple case */ | |
block.realtime = last_realtime + deltaticks * current_tempo->tempo / target.tickdiv; | |
} else { | |
/* Yes, new tempo */ | |
MidiFile::TempoChange *new_tempo = current_tempo + 1; | |
block.realtime = last_realtime; | |
block.realtime += (new_tempo->ticktime - last_ticktime) * current_tempo->tempo / target.tickdiv; | |
block.realtime += (block.ticktime - new_tempo->ticktime) * new_tempo->tempo / target.tickdiv; | |
current_tempo = new_tempo; | |
} | |
last_ticktime = block.ticktime; | |
last_realtime = block.realtime; | |
} | |
return true; | |
} | |
/** | |
* Read the header of a standard MIDI file. | |
* @param[in] filename name of file to read from | |
* @param[out] header filled with data read | |
* @return true if the file could be opened and contained a header with correct format | |
*/ | |
bool MidiFile::ReadSMFHeader(const char *filename, SMFHeader &header) { | |
FILE *file = FioFOpenFile(filename, "rb", Subdirectory::BASESET_DIR); | |
if (!file) return false; | |
bool result = ReadSMFHeader(file, header); | |
FioFCloseFile(file); | |
return result; | |
} | |
/** | |
* Read the header of a standard MIDI file. | |
* The function will consume 14 bytes from the current file pointer position. | |
* @param[in] file open file to read from (should be in binary mode) | |
* @param[out] header filled with data read | |
* @return true if a header in correct format could be read from the file | |
*/ | |
bool MidiFile::ReadSMFHeader(FILE *file, SMFHeader &header) { | |
/* Try to read header, fixed size */ | |
byte buffer[14]; | |
if (fread(buffer, sizeof(buffer), 1, file) != 1) | |
return false; | |
/* Check magic, 'MThd' followed by 4 byte length indicator (always = 6 in SMF) */ | |
const byte magic[] = { 'M', 'T', 'h', 'd', 0x00, 0x00, 0x00, 0x06 }; | |
if (MemCmpT(buffer, magic, sizeof(magic)) != 0) | |
return false; | |
/* Read the parameters of the file */ | |
header.format = (buffer[8] << 8) | buffer[9]; | |
header.tracks = (buffer[10] << 8) | buffer[11]; | |
header.tickdiv = (buffer[12] << 8) | buffer[13]; | |
return true; | |
} | |
extern byte * GetCatEntryData(const char *filename, size_t entrynum, size_t &entrylen); /* from music.cpp */ | |
/** | |
* 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) { | |
_midifile_instance = this; | |
const char *starpos = strchr(filename, '*'); | |
if (starpos) { | |
/* This is not an SMF, but a DOS version format song from a CAT file */ | |
int entrynum = atoi(starpos + 1); | |
char *fn = stredup(filename, starpos - 1); | |
const char *basefn = strrchr(fn, PATHSEPCHAR); | |
basefn = basefn ? (basefn + 1) : fn; | |
size_t datalen; | |
byte *data = GetCatEntryData(basefn, entrynum, datalen); | |
free(fn); | |
if (!data) return false; | |
bool res = this->LoadMpsData(data, datalen); | |
free(data); | |
return res; | |
} | |
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; | |
} | |
struct MpsMachine { | |
struct Channel { | |
byte cur_program; ///< program selected, used for velocity scaling (lookup into programvelocities array) | |
byte running_status; ///< last midi status code seen | |
uint16 delay; ///< frames until next command | |
uint32 playpos; ///< next byte to play this channel from | |
uint32 startpos; ///< start position of master track | |
uint32 returnpos; ///< next return position after playing a segment | |
Channel() : cur_program(0xFF), running_status(0), delay(0), playpos(0), startpos(0), returnpos(0) { } | |
}; | |
Channel channels[16]; | |
std::vector<uint32> segments; ///< pointers into songdata to repeatable data segments | |
int16 tempo_ticks; ///< ticker that increments when playing a frame, decrements before playing a frame | |
uint16 song_tempo; ///< threshold for actually playing a frame | |
bool shouldplayflag; ///< not-end-of-song | |
static const byte programvelocities[128]; | |
static int glitch[2]; | |
const byte *songdata; ///< raw data array | |
size_t songdatalen; ///< length of song data | |
MidiFile ⌖ ///< recipient of data | |
static void AddMidiData(MidiFile::DataBlock &block, byte b1) { | |
*block.data.Append() = b1; | |
} | |
static void AddMidiData(MidiFile::DataBlock &block, byte b1, byte b2) { | |
*block.data.Append() = b1; | |
*block.data.Append() = b2; | |
} | |
static void AddMidiData(MidiFile::DataBlock &block, byte b1, byte b2, byte b3) { | |
*block.data.Append() = b1; | |
*block.data.Append() = b2; | |
*block.data.Append() = b3; | |
} | |
/** | |
* Construct a TTD DOS music format decoder. | |
* @param songdata Buffer of song data from CAT file, ownership remains with caller | |
* @param songdatalen Length of the data buffer in bytes | |
* @param target MidiFile object to add decoded data to | |
*/ | |
MpsMachine(const byte *data, size_t length, MidiFile &target) | |
: songdata(data), songdatalen(length), target(target) | |
{ | |
uint32 pos = 0; | |
int loopmax; | |
int loopidx; | |
/* First byte is the initial "tempo" */ | |
this->song_tempo = this->songdata[pos++]; | |
/* Next byte is a count of callable segments */ | |
loopmax = this->songdata[pos++]; | |
for (loopidx = 0; loopidx < loopmax; loopidx++) { | |
/* Segments form a linked list in the stream, | |
* first two bytes in each is an offset to the next. | |
* Two bytes between offset to next and start of data | |
* are unaccounted for. */ | |
this->segments.push_back(pos + 4); | |
pos += FROM_LE16(*(const int16 *)(this->songdata + pos)); | |
} | |
/* After segments follows list of master tracks for each channel, | |
* also prefixed with a byte counting actual tracks. */ | |
loopmax = this->songdata[pos++]; | |
for (loopidx = 0; loopidx < loopmax; loopidx++) { | |
/* Similar structure to segments list, but also has | |
* the MIDI channel number as a byte before the offset | |
* to next track. */ | |
byte ch = this->songdata[pos++]; | |
this->channels[ch].startpos = pos + 4; | |
pos += FROM_LE16(*(const int16 *)(this->songdata + pos)); | |
} | |
} | |
/** | |
* Read an SMF-style variable length value (note duration) from songdata. | |
* @param pos Position to read from, updated to point to next byte after the value read | |
* @return Value read from data stream | |
*/ | |
uint16 ReadVariableLength(uint32 &pos) { | |
byte b = 0; | |
uint16 res = 0; | |
do { | |
b = this->songdata[pos++]; | |
res = (res << 7) + (b & 0x7F); | |
} while (b & 0x80); | |
return res; | |
} | |
/** | |
* Prepare for playback from the beginning. Resets the song pointer for every track to the beginning. | |
*/ | |
void RestartSong() { | |
for (int ch = 0; ch < 16; ch++) { | |
Channel &chandata = this->channels[ch]; | |
if (chandata.startpos != 0) { | |
/* Active track, set position to beginning */ | |
chandata.playpos = chandata.startpos; | |
chandata.delay = this->ReadVariableLength(chandata.playpos); | |
} else { | |
/* Inactive track, mark as such */ | |
chandata.playpos = 0; | |
chandata.delay = 0; | |
} | |
} | |
} | |
/** | |
* Play one frame of data from one channel | |
*/ | |
uint16 PlayChannelFrame(MidiFile::DataBlock &outblock, int channel) { | |
uint16 newdelay = 0; | |
byte b1, b2; | |
Channel &chandata = this->channels[channel]; | |
do { | |
/* Read command/status byte */ | |
b1 = this->songdata[chandata.playpos++]; | |
/* Command 0xFE, call segment from master track */ | |
if (b1 == 0xFE) { | |
b1 = this->songdata[chandata.playpos++]; | |
chandata.returnpos = chandata.playpos; | |
/* "glitch" values can cause intentional misplaying, try it :) */ | |
chandata.playpos = this->segments[(b1 * glitch[1] + glitch[0]) % this->segments.size()]; | |
newdelay = this->ReadVariableLength(chandata.playpos); | |
if (newdelay == 0) { | |
continue; | |
} | |
return newdelay; | |
} | |
/* Command 0xFD, return from segment to master track */ | |
if (b1 == 0xFD) { | |
chandata.playpos = chandata.returnpos; | |
chandata.returnpos = 0; | |
newdelay = this->ReadVariableLength(chandata.playpos); | |
if (newdelay == 0) { | |
continue; | |
} | |
return newdelay; | |
} | |
/* Command 0xFF, end of song */ | |
if (b1 == 0xFF) { | |
this->shouldplayflag = false; | |
return 0; | |
} | |
/* Regular MIDI channel message status byte */ | |
if (b1 >= 0x80) { | |
/* Save the status byte as running status for the channel | |
* and read another byte for first parameter to command */ | |
chandata.running_status = b1; | |
b1 = this->songdata[chandata.playpos++]; | |
} | |
switch (chandata.running_status & 0xF0) { | |
case MIDIST_NOTEOFF: | |
case MIDIST_NOTEON: | |
b2 = this->songdata[chandata.playpos++]; | |
if (b2 != 0) { | |
/* Note on, read velocity and scale according to rules */ | |
int16 velocity; | |
if (channel == 9) { | |
/* Percussion */ | |
velocity = (int16)b2 * 0x50; | |
} else { | |
/* Regular channel */ | |
velocity = b2 * programvelocities[chandata.cur_program]; | |
} | |
b2 = (velocity / 128) & 0x00FF; | |
AddMidiData(outblock, MIDIST_NOTEON + channel, b1, b2); | |
} else { | |
/* Note off */ | |
AddMidiData(outblock, MIDIST_NOTEON + channel, b1, 0); | |
} | |
break; | |
case MIDIST_CONTROLLER: | |
b2 = this->songdata[chandata.playpos++]; | |
if (b1 == 0x7E) { | |
/* Unknown what the purpose of this is. | |
* Occurs in "Can't get There from Here" and | |
* in "Aliens Ate my Railway" a few times each. | |
* General MIDI controller 0x7E is "Mono mode on" | |
* so maybe intended for more limited synths. | |
*/ | |
break; | |
} else if (b1 == 0) { | |
/* Special case for tempo change, usually | |
* controller 0 is "Bank select". */ | |
if (b2 != 0) { | |
this->song_tempo = ((int)b2) * 48 / 60; | |
} | |
break; | |
} else if (b1 == 0x5B) { | |
/* Controller 0x5B is "Reverb send level", | |
* this just enables reverb to a fixed level. */ | |
b2 = 0x1E; | |
} | |
AddMidiData(outblock, MIDIST_CONTROLLER + channel, b1, b2); | |
break; | |
case MIDIST_PROGCHG: | |
if (b1 == 0x7E) { | |
/* Program change to "Applause" is originally used | |
* to cause the song to loop, but that gets handled | |
* separately in the output driver here. | |
* Just end the song. */ | |
this->shouldplayflag = false; | |
break; | |
} | |
/* Used for note velocity scaling lookup */ | |
chandata.cur_program = b1; | |
/* Two programs translated to a third, this is likely to | |
* provide three different velocity scalings of "brass". */ | |
if (b1 == 0x57 || b1 == 0x3F) { | |
b1 = 0x3E; | |
} | |
AddMidiData(outblock, MIDIST_PROGCHG + channel, b1); | |
break; | |
case MIDIST_PITCHBEND: | |
b2 = this->songdata[chandata.playpos++]; | |
AddMidiData(outblock, MIDIST_PITCHBEND + channel, b1, b2); | |
break; | |
default: | |
break; | |
} | |
newdelay = this->ReadVariableLength(chandata.playpos); | |
} while (newdelay == 0); | |
return newdelay; | |
} | |
/** | |
* Play one frame of data into a block. | |
*/ | |
bool PlayFrame(MidiFile::DataBlock &block) { | |
/* Update tempo/ticks counter */ | |
this->tempo_ticks -= this->song_tempo; | |
if (this->tempo_ticks > 0) { | |
return true; | |
} | |
this->tempo_ticks += 148; | |
/* Look over all channels, play those active */ | |
for (int ch = 0; ch < 16; ch++) { | |
Channel &chandata = this->channels[ch]; | |
if (chandata.playpos != 0) { | |
if (chandata.delay == 0) { | |
chandata.delay = this->PlayChannelFrame(block, ch); | |
} | |
chandata.delay--; | |
} | |
} | |
return this->shouldplayflag; | |
} | |
/** | |
* Perform playback of whole song. | |
*/ | |
bool PlayInto() { | |
/* Guessed values based on what sounds right. | |
* A tickdiv of 96 is common, and 6.5 ms per tick | |
* leads to an initial musical tempo of ~96 bpm. | |
* However this has very little relation to the actual | |
* tempo of the music, since that gets controlled by | |
* the "other" tempo values read from the data. How | |
* those values relate to actual musical tempo has | |
* yet to be discovered. */ | |
this->target.tickdiv = 96; | |
this->target.tempos.push_back(MidiFile::TempoChange(0, 6500*this->target.tickdiv)); | |
/* Initialize playback simulation */ | |
this->RestartSong(); | |
this->shouldplayflag = true; | |
this->song_tempo = (int32)this->song_tempo * 24 / 60; | |
this->tempo_ticks = this->song_tempo; | |
/* Always reset percussion channel to program 0 */ | |
this->target.blocks.push_back(MidiFile::DataBlock()); | |
AddMidiData(this->target.blocks.back(), MIDIST_PROGCHG+9, 0x00); | |
/* Technically should be an endless loop, but having | |
* a maximum (about 10 minutes) avoids getting stuck, | |
* in case of corrupted data. */ | |
for (uint32 tick = 0; tick < 100000; tick+=1) { | |
this->target.blocks.push_back(MidiFile::DataBlock()); | |
auto &block = this->target.blocks.back(); | |
block.ticktime = tick; | |
if (!this->PlayFrame(block)) | |
break; | |
} | |
return true; | |
} | |
}; | |
/* Base note velocities for various GM programs */ | |
const byte MpsMachine::programvelocities[128] = { | |
100,100,100,100,100, 90,100,100,100,100,100, 90,100,100,100,100, | |
100,100, 85,100,100,100,100,100,100,100,100,100, 90, 90,110, 80, | |
100,100,100, 90, 70,100,100,100,100,100,100,100,100,100,100,100, | |
100,100, 90,100,100,100,100,100,100,120,100,100,100,120,100,127, | |
100,100, 90,100,100,100,100,100,100, 95,100,100,100,100,100,100, | |
100,100,100,100,100,100,100,115,100,100,100,100,100,100,100,100, | |
100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100, | |
100,100,100,100,100,100,100,100,100,100,100,100,100,100,100,100, | |
}; | |
int MpsMachine::glitch[2] = { 0, 1 }; | |
/** | |
* Create MIDI data from song data for the original Microprose music drivers. | |
* @param data pointer to block of data | |
* @param length size of data in bytes | |
* @return true if the data could be loaded | |
*/ | |
bool MidiFile::LoadMpsData(const byte *data, size_t length) { | |
MpsMachine machine(data, length, *this); | |
return machine.PlayInto() && FixupMidiData(*this); | |
} | |
/** | |
* 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; | |
_midifile_instance = this; | |
other.blocks.clear(); | |
other.tempos.clear(); | |
other.tickdiv = 0; | |
} | |
static void WriteVariableLen(FILE *f, uint32 value) { | |
if (value < 0x7F) { | |
byte tb = value; | |
fwrite(&tb, 1, 1, f); | |
} | |
else if (value < 0x3FFF) { | |
byte tb[2]; | |
tb[1] = value & 0x7F; value >>= 7; | |
tb[0] = (value & 0x7F) | 0x80; value >>= 7; | |
fwrite(tb, 1, sizeof(tb), f); | |
} | |
else if (value < 0x1FFFFF) { | |
byte tb[3]; | |
tb[2] = value & 0x7F; value >>= 7; | |
tb[1] = (value & 0x7F) | 0x80; value >>= 7; | |
tb[0] = (value & 0x7F) | 0x80; value >>= 7; | |
fwrite(tb, 1, sizeof(tb), f); | |
} | |
else if (value < 0x0FFFFFFF) { | |
byte tb[4]; | |
tb[3] = value & 0x7F; value >>= 7; | |
tb[2] = (value & 0x7F) | 0x80; value >>= 7; | |
tb[1] = (value & 0x7F) | 0x80; value >>= 7; | |
tb[0] = (value & 0x7F) | 0x80; value >>= 7; | |
fwrite(tb, 1, sizeof(tb), f); | |
} | |
} | |
/** | |
* Write a Standard MIDI File containing the decoded music. | |
* @param filename Name of file to write to | |
* @return True if the file was written to completion | |
*/ | |
bool MidiFile::WriteSMF(const char *filename) { | |
FILE *f = FioFOpenFile(filename, "wb", Subdirectory::NO_DIRECTORY); | |
if (!f) { | |
return false; | |
} | |
uint16 u16; | |
byte bb; | |
/* SMF header */ | |
const byte filemagic[] = { 'M', 'T', 'h', 'd', 0x00, 0x00, 0x00, 0x06 }; | |
fwrite(filemagic, sizeof(filemagic), 1, f); | |
fwrite(&(u16 = 0), sizeof(u16), 1, f); | |
fwrite(&(u16 = TO_BE16(1)), sizeof(u16), 1, f); | |
fwrite(&(u16 = TO_BE16(this->tickdiv)), sizeof(u16), 1, f); | |
/* Track header, block length is written after everything | |
* else has already been written and length is known. */ | |
const byte trackmagic[] = { 'M', 'T', 'r', 'k', 0, 0, 0, 0 }; | |
fwrite(trackmagic, sizeof(trackmagic), 1, f); | |
size_t tracksizepos = ftell(f) - 4; | |
/* Write blocks in sequence */ | |
uint32 lasttime = 0; | |
size_t nexttempoindex = 0; | |
for (size_t bi = 0; bi < this->blocks.size(); bi++) { | |
redoblock: | |
DataBlock &block = this->blocks[bi]; | |
TempoChange &nexttempo = this->tempos[nexttempoindex]; | |
uint32 timediff = block.ticktime - lasttime; | |
/* Check if there is a tempo change before this block */ | |
if (nexttempo.ticktime < block.ticktime) { | |
timediff = nexttempo.ticktime - lasttime; | |
} | |
/* Write delta time for block */ | |
lasttime += timediff; | |
bool needtime = false; | |
WriteVariableLen(f, timediff); | |
/* Write tempo change if there is one */ | |
if (nexttempo.ticktime <= block.ticktime) { | |
byte tempobuf[6] = { MIDIST_SMF_META, 0x51, 0x03, 0, 0, 0 }; | |
tempobuf[3] = (nexttempo.tempo & 0x00FF0000) >> 16; | |
tempobuf[4] = (nexttempo.tempo & 0x0000FF00) >> 8; | |
tempobuf[5] = (nexttempo.tempo & 0x000000FF); | |
fwrite(tempobuf, 1, 6, f); | |
nexttempoindex++; | |
needtime = true; | |
} | |
/* If a tempo change occurred between two blocks, rather than | |
* at start of this one, start over with delta time for the block. */ | |
if (nexttempo.ticktime < block.ticktime) { | |
goto redoblock; | |
} | |
/* Write each block data command */ | |
byte *dp = block.data.Begin(); | |
while (dp < block.data.End()) { | |
/* Always zero delta time inside blocks */ | |
if (needtime) { | |
fwrite(&(bb = 0), 1, 1, f); | |
} | |
needtime = true; | |
/* Check message type and write appropriate number of bytes */ | |
switch (*dp & 0xF0) { | |
case MIDIST_NOTEOFF: | |
case MIDIST_NOTEON: | |
case MIDIST_POLYPRESS: | |
case MIDIST_CONTROLLER: | |
case MIDIST_PITCHBEND: | |
fwrite(dp, 1, 3, f); | |
dp += 3; | |
continue; | |
case MIDIST_PROGCHG: | |
case MIDIST_CHANPRESS: | |
fwrite(dp, 1, 2, f); | |
dp += 2; | |
continue; | |
} | |
/* Sysex needs to measure length and write that as well */ | |
if (*dp == MIDIST_SYSEX) { | |
fwrite(dp, 1, 1, f); | |
dp++; | |
byte *sysexend = dp; | |
while (*sysexend++ != MIDIST_ENDSYSEX); | |
ptrdiff_t sysexlen = sysexend - dp; | |
WriteVariableLen(f, sysexlen); | |
fwrite(dp, 1, sysexend - dp, f); | |
dp = sysexend; | |
continue; | |
} | |
/* Fail for any other commands */ | |
fclose(f); | |
return false; | |
} | |
} | |
/* End of track marker */ | |
fwrite(&(bb = 0x00), 1, 1, f); | |
fwrite(&(bb = MIDIST_SMF_META), 1, 1, f); | |
fwrite(&(bb = 0x2F), 1, 1, f); | |
fwrite(&(bb = 0x00), 1, 1, f); | |
/* Fill out the RIFF block length */ | |
size_t trackendpos = ftell(f); | |
fseek(f, tracksizepos, SEEK_SET); | |
uint32 tracksize = trackendpos - tracksizepos - 4; | |
tracksize = TO_BE32(tracksize); | |
fwrite(&tracksize, 4, 1, f); | |
fclose(f); | |
return true; | |
} | |
void RegisterConsoleMidiCommands(); | |
MidiFile::MidiFile() { | |
RegisterConsoleMidiCommands(); | |
} | |
MidiFile::~MidiFile() { | |
if (_midifile_instance == this) | |
_midifile_instance = NULL; | |
} | |
#include "../console_func.h" | |
#include "../console_internal.h" | |
static bool CmdDumpSMF(byte argc, char *argv[]) { | |
if (argc == 0) { | |
IConsolePrint(CC_WARNING, "Write the current song to a Standard MIDI File. Usage: 'dumpsmf <filename>'"); | |
return true; | |
} | |
if (argc != 2) { | |
IConsolePrint(CC_WARNING, "You must specify a filename to write MIDI data to."); | |
return false; | |
} | |
if (_midifile_instance == NULL) { | |
IConsolePrint(CC_ERROR, "There is no MIDI file loaded currently, make sure music is playing, and you're using a driver that works with raw MIDI."); | |
return false; | |
} | |
char fnbuf[MAX_PATH] = { 0 }; | |
FioGetFullPath(fnbuf, lastof(fnbuf), Searchpath::SP_PERSONAL_DIR, Subdirectory::SCREENSHOT_DIR, argv[1]); | |
IConsolePrintF(CC_INFO, "Dumping MIDI to: %s", fnbuf); | |
if (_midifile_instance->WriteSMF(fnbuf)) { | |
IConsolePrint(CC_INFO, "File written successfully."); | |
return true; | |
} else { | |
IConsolePrint(CC_ERROR, "An error occurred writing MIDI file."); | |
return false; | |
} | |
} | |
static bool CmdGlitchMusic(byte argc, char *argv[]) { | |
if (argc < 2) { | |
IConsolePrint(CC_WARNING, "Make music from the DOS version glitchy. Usage: 'glitchmusic <num1> [<num2>]'"); | |
IConsolePrint(CC_WARNING, "If the first number is 0, glitching is disabled. Otherwise both must be positive integers."); | |
return true; | |
} | |
int arg1 = 0, arg2 = 1; | |
arg1 = atoi(argv[1]); | |
if (argc >= 3 && arg1 != 0) { | |
arg2 = atoi(argv[2]); | |
} | |
if (arg1 < 0) { | |
IConsolePrint(CC_ERROR, "First argument must be 0 or greater"); | |
return false; | |
} | |
if (arg2 < 1) { | |
IConsolePrint(CC_ERROR, "Second argument must be 1 or greater"); | |
return false; | |
} | |
MpsMachine::glitch[0] = arg1; | |
MpsMachine::glitch[1] = arg2; | |
return true; | |
} | |
static void RegisterConsoleMidiCommands() { | |
static bool registered = false; | |
if (!registered) { | |
IConsoleCmdRegister("dumpsmf", CmdDumpSMF); | |
IConsoleCmdRegister("glitchmusic", CmdGlitchMusic); | |
registered = true; | |
} | |
} |
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
/* $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) { } | |
}; | |
struct SMFHeader { | |
uint16 format; | |
uint16 tracks; | |
uint16 tickdiv; | |
}; | |
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 | |
MidiFile(); | |
~MidiFile(); | |
bool LoadFile(const char *filename); | |
bool LoadMpsData(const byte *data, size_t length); | |
void MoveFrom(MidiFile &other); | |
bool WriteSMF(const char *filename); | |
static bool ReadSMFHeader(const char *filename, SMFHeader &header); | |
static bool ReadSMFHeader(FILE *file, SMFHeader &header); | |
}; |
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
/* $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 win32_m.cpp Music playback for Windows. */ | |
#include "../stdafx.h" | |
#include "../string_func.h" | |
#include "win32_m.h" | |
#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 { | |
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; | |
static byte ScaleVolume(byte original, byte scale) { | |
return original * scale / 127; | |
} | |
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); | |
} | |
} | |
static void TransmitChannelMsg(byte status, byte p1, byte p2 = 0) { | |
midiOutShortMsg(_midi.midi_out, status | (p1 << 8) | (p2 << 16)); | |
} | |
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; | |
} | |
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; | |
} | |
/* 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; | |
} | |
} | |
} | |
void MusicDriver_Win32::PlaySong(const char *filename, int time_start, int time_end, bool loop) | |
{ | |
DEBUG(driver, 2, "Win32-MIDI: PlaySong: entry"); | |
EnterCriticalSection(&_midi.lock); | |
_midi.next_file.LoadFile(filename); | |
_midi.next_segment.start = time_start; | |
_midi.next_segment.end = time_end; | |
_midi.next_segment.loop = loop; | |
DEBUG(driver, 2, "Win32-MIDI: PlaySong: setting flag"); | |
_midi.do_start = true; | |
if (_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); | |
} | |
void MusicDriver_Win32::StopSong() | |
{ | |
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); | |
} | |
bool MusicDriver_Win32::IsSongPlaying() | |
{ | |
return _midi.playing || _midi.do_start; | |
} | |
void MusicDriver_Win32::SetVolume(byte vol) | |
{ | |
EnterCriticalSection(&_midi.lock); | |
_midi.new_volume = vol; | |
LeaveCriticalSection(&_midi.lock); | |
} | |
const char *MusicDriver_Win32::Start(const char * const *parm) | |
{ | |
DEBUG(driver, 2, "Win32-MIDI: Start: initializing"); | |
InitializeCriticalSection(&_midi.lock); | |
int resolution = GetDriverParamInt(parm, "resolution", 5); | |
int port = GetDriverParamInt(parm, "port", -1); | |
UINT devid; | |
if (port < 0) { | |
devid = MIDI_MAPPER; | |
} else { | |
devid = (UINT)port; | |
} | |
resolution = Clamp(resolution, 1, 20); | |
if (midiOutOpen(&_midi.midi_out, devid, (DWORD_PTR)&MidiOutProc, (DWORD_PTR)this, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) | |
return "could not open midi device"; | |
{ | |
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); | |
} | |
TIMECAPS timecaps; | |
if (timeGetDevCaps(&timecaps, sizeof(timecaps)) == MMSYSERR_NOERROR) { | |
_midi.time_period = min(max((UINT)resolution, timecaps.wPeriodMin), timecaps.wPeriodMax); | |
if (timeBeginPeriod(_midi.time_period) == MMSYSERR_NOERROR) { | |
DEBUG(driver, 2, "Win32-MIDI: Start: timer resolution is %d", (int)_midi.time_period); | |
return NULL; | |
} | |
} | |
midiOutClose(_midi.midi_out); | |
return "could not set timer resolution"; | |
} | |
void MusicDriver_Win32::Stop() | |
{ | |
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); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment