Skip to content

Instantly share code, notes, and snippets.

@nielsmh nielsmh/midifile.cpp Secret
Created Mar 16, 2018

Embed
What would you like to do?
OTTD new music code
/* $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 &target; ///< 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;
}
}
/* $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);
};
/* $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
You can’t perform that action at this time.