10#include "midifile.hpp"
11#include "../fileio_func.h"
12#include "../fileio_type.h"
13#include "../string_func.h"
14#include "../core/endian_func.hpp"
15#include "../core/mem_func.hpp"
16#include "../base_media_base.h"
19#include "../console_func.h"
20#include "../console_internal.h"
25static MidiFile *_midifile_instance =
nullptr;
33const uint8_t *MidiGetStandardSysexMessage(MidiSysexMessage msg,
size_t &length)
35 static uint8_t reset_gm_sysex[] = { 0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7 };
36 static uint8_t reset_gs_sysex[] = { 0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7 };
37 static uint8_t reset_xg_sysex[] = { 0xF0, 0x43, 0x10, 0x4C, 0x00, 0x00, 0x7E, 0x00, 0xF7 };
38 static uint8_t roland_reverb_sysex[] = { 0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x01, 0x30, 0x02, 0x04, 0x00, 0x40, 0x40, 0x00, 0x00, 0x09, 0xF7 };
41 case MidiSysexMessage::ResetGM:
43 return reset_gm_sysex;
44 case MidiSysexMessage::ResetGS:
46 return reset_gs_sysex;
47 case MidiSysexMessage::ResetXG:
49 return reset_xg_sysex;
50 case MidiSysexMessage::RolandSetReverb:
51 length =
lengthof(roland_reverb_sysex);
52 return roland_reverb_sysex;
63 std::vector<uint8_t> buf{};
75 this->buf.resize(len);
76 if (fread(this->buf.data(), 1, len, file) != len) {
88 return !this->buf.empty();
97 return this->pos >= this->buf.size();
107 if (this->
IsEnd())
return false;
108 b = this->buf[this->pos++];
124 if (this->
IsEnd())
return false;
125 b = this->buf[this->pos++];
126 res = (res << 7) | (b & 0x7F);
139 if (this->
IsEnd())
return false;
140 if (this->buf.size() - this->pos < length)
return false;
141 std::copy(std::begin(this->buf) + this->pos, std::begin(this->buf) + this->pos + length, dest);
154 if (this->
IsEnd())
return false;
155 if (this->buf.size() - this->pos < length)
return false;
156 dest->
data.insert(dest->
data.end(), std::begin(this->buf) + this->pos, std::begin(this->buf) + this->pos + length);
168 if (this->
IsEnd())
return false;
169 if (this->buf.size() - this->pos < count)
return false;
181 if (count > this->pos)
return false;
191 const uint8_t magic[] = {
'M',
'T',
'r',
'k' };
192 if (fread(buf,
sizeof(magic), 1, file) != 1) {
195 if (memcmp(magic, buf,
sizeof(magic)) != 0) {
200 uint32_t chunk_length;
201 if (fread(&chunk_length, 1, 4, file) != 4) {
204 chunk_length = FROM_BE32(chunk_length);
207 if (!chunk.IsValid()) {
213 uint8_t last_status = 0;
214 bool running_sysex =
false;
215 while (!chunk.IsEnd()) {
217 uint32_t deltatime = 0;
218 if (!chunk.ReadVariableLength(deltatime)) {
227 if (!chunk.ReadByte(status)) {
231 if ((status & 0x80) == 0) {
235 status = last_status;
237 }
else if ((status & 0xF0) != 0xF0) {
239 last_status = status;
241 switch (status & 0xF0) {
244 case MIDIST_POLYPRESS:
245 case MIDIST_CONTROLLER:
246 case MIDIST_PITCHBEND:
248 block->
data.push_back(status);
249 if (!chunk.ReadDataBlock(block, 2)) {
254 case MIDIST_CHANPRESS:
256 block->
data.push_back(status);
257 if (!chunk.ReadByte(buf[0])) {
260 block->
data.push_back(buf[0]);
265 }
else if (status == MIDIST_SMF_META) {
267 if (!chunk.ReadByte(buf[0])) {
271 if (!chunk.ReadVariableLength(length)) {
277 return (length == 0);
280 if (length != 3)
return false;
281 if (!chunk.ReadBuffer(buf, 3))
return false;
286 if (!chunk.Skip(length)) {
291 }
else if (status == MIDIST_SYSEX || (status == MIDIST_SMF_ESCAPE && running_sysex)) {
294 if (!chunk.ReadVariableLength(length)) {
297 block->
data.push_back(0xF0);
298 if (!chunk.ReadDataBlock(block, length)) {
301 if (block->
data.back() != 0xF7) {
303 running_sysex =
true;
304 block->
data.push_back(0xF7);
306 running_sysex =
false;
308 }
else if (status == MIDIST_SMF_ESCAPE) {
311 if (!chunk.ReadVariableLength(length)) {
314 if (!chunk.ReadDataBlock(block, length)) {
335bool TicktimeAscending(
const T &a,
const T &b)
337 return a.ticktime < b.ticktime;
340static bool FixupMidiData(
MidiFile &target)
343 std::sort(target.
tempos.begin(), target.
tempos.end(), TicktimeAscending<MidiFile::TempoChange>);
344 std::sort(target.
blocks.begin(), target.
blocks.end(), TicktimeAscending<MidiFile::DataBlock>);
346 if (target.
tempos.empty()) {
354 std::vector<MidiFile::DataBlock> merged_blocks;
355 uint32_t last_ticktime = 0;
356 for (
size_t i = 0; i < target.
blocks.size(); i++) {
358 if (block.
data.empty()) {
360 }
else if (block.
ticktime > last_ticktime || merged_blocks.empty()) {
361 merged_blocks.push_back(block);
364 merged_blocks.back().data.insert(merged_blocks.back().data.end(), block.
data.begin(), block.
data.end());
367 std::swap(merged_blocks, target.
blocks);
371 uint32_t last_realtime = 0;
372 size_t cur_tempo = 0, cur_block = 0;
373 while (cur_block < target.
blocks.size()) {
379 int64_t tickdiff = block.
ticktime - last_ticktime;
381 last_realtime += uint32_t(tickdiff * tempo.
tempo / target.
tickdiv);
386 int64_t tickdiff = next_tempo.
ticktime - last_ticktime;
387 last_ticktime = next_tempo.
ticktime;
388 last_realtime += uint32_t(tickdiff * tempo.
tempo / target.
tickdiv);
405 if (!file.has_value())
return false;
421 if (fread(buffer,
sizeof(buffer), 1, file) != 1) {
426 const uint8_t magic[] = {
'M',
'T',
'h',
'd', 0x00, 0x00, 0x00, 0x06 };
427 if (
MemCmpT(buffer, magic,
sizeof(magic)) != 0) {
432 header.format = (buffer[8] << 8) | buffer[9];
433 header.tracks = (buffer[10] << 8) | buffer[11];
434 header.tickdiv = (buffer[12] << 8) | buffer[13];
445 _midifile_instance =
this;
452 if (!file.has_value())
return false;
458 if (header.format != 0 && header.format != 1)
return false;
460 if ((header.tickdiv & 0x8000) != 0)
return false;
462 this->
tickdiv = header.tickdiv;
464 for (; header.tracks > 0; header.tracks--) {
465 if (!ReadTrackChunk(*file, *
this)) {
470 return FixupMidiData(*
this);
528 block.
data.push_back(b1);
529 block.
data.push_back(b2);
533 block.
data.push_back(b1);
534 block.
data.push_back(b2);
535 block.
data.push_back(b3);
552 this->initial_tempo = this->songdata[pos++];
555 loopmax = this->songdata[pos++];
556 for (loopidx = 0; loopidx < loopmax; loopidx++) {
562 pos += FROM_LE16(*(
const int16_t *)(this->songdata + pos));
567 loopmax = this->songdata[pos++];
568 for (loopidx = 0; loopidx < loopmax; loopidx++) {
572 uint8_t ch = this->songdata[pos++];
573 this->
channels[ch].startpos = pos + 4;
574 pos += FROM_LE16(*(
const int16_t *)(this->songdata + pos));
588 b = this->songdata[pos++];
589 res = (res << 7) + (b & 0x7F);
599 for (
int ch = 0; ch < 16; ch++) {
618 uint16_t newdelay = 0;
624 b1 = this->songdata[chandata.
playpos++];
628 b1 = this->songdata[chandata.
playpos++];
651 this->shouldplayflag =
false;
660 b1 = this->songdata[chandata.
playpos++];
666 b2 = this->songdata[chandata.
playpos++];
672 velocity = (int16_t)b2 * 0x50;
677 b2 = (velocity / 128) & 0x00FF;
678 AddMidiData(outblock, MIDIST_NOTEON + channel, b1, b2);
681 AddMidiData(outblock, MIDIST_NOTEON + channel, b1, 0);
684 case MIDIST_CONTROLLER:
685 b2 = this->songdata[chandata.
playpos++];
686 if (b1 == MIDICT_MODE_MONO) {
692 }
else if (b1 == 0) {
696 this->current_tempo = ((int)b2) * 48 / 60;
699 }
else if (b1 == MIDICT_EFFECTS1) {
704 AddMidiData(outblock, MIDIST_CONTROLLER + channel, b1, b2);
712 this->shouldplayflag =
false;
719 if (b1 == 0x57 || b1 == 0x3F) {
722 AddMidiData(outblock, MIDIST_PROGCHG + channel, b1);
724 case MIDIST_PITCHBEND:
725 b2 = this->songdata[chandata.
playpos++];
726 AddMidiData(outblock, MIDIST_PITCHBEND + channel, b1, b2);
733 }
while (newdelay == 0);
745 if (this->tempo_ticks > 0) {
751 for (
int ch = 0; ch < 16; ch++) {
754 if (chandata.
delay == 0) {
778 this->shouldplayflag =
true;
779 this->current_tempo = (int32_t)this->initial_tempo * 24 / 60;
784 AddMidiData(this->target.
blocks.back(), MIDIST_PROGCHG + 9, 0x00);
789 for (uint32_t tick = 0; tick < 100000; tick += 1) {
790 auto &block = this->target.
blocks.emplace_back();
803 100, 100, 100, 100, 100, 90, 100, 100, 100, 100, 100, 90, 100, 100, 100, 100,
804 100, 100, 85, 100, 100, 100, 100, 100, 100, 100, 100, 100, 90, 90, 110, 80,
805 100, 100, 100, 90, 70, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
806 100, 100, 90, 100, 100, 100, 100, 100, 100, 120, 100, 100, 100, 120, 100, 127,
807 100, 100, 90, 100, 100, 100, 100, 100, 100, 95, 100, 100, 100, 100, 100, 100,
808 100, 100, 100, 100, 100, 100, 100, 115, 100, 100, 100, 100, 100, 100, 100, 100,
809 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
810 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
821 _midifile_instance =
this;
824 return machine.
PlayInto() && FixupMidiData(*
this);
835 if (songdata.has_value()) {
836 bool result = this->
LoadMpsData(songdata->data(), songdata->size());
857 _midifile_instance =
this;
864static void WriteVariableLen(
FileHandle &f, uint32_t value)
868 fwrite(&tb, 1, 1, f);
869 }
else if (value <= 0x3FFF) {
871 tb[1] = value & 0x7F; value >>= 7;
872 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
873 fwrite(tb, 1,
sizeof(tb), f);
874 }
else if (value <= 0x1FFFFF) {
876 tb[2] = value & 0x7F; value >>= 7;
877 tb[1] = (value & 0x7F) | 0x80; value >>= 7;
878 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
879 fwrite(tb, 1,
sizeof(tb), f);
880 }
else if (value <= 0x0FFFFFFF) {
882 tb[3] = value & 0x7F; value >>= 7;
883 tb[2] = (value & 0x7F) | 0x80; value >>= 7;
884 tb[1] = (value & 0x7F) | 0x80; value >>= 7;
885 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
886 fwrite(tb, 1,
sizeof(tb), f);
898 if (!of.has_value())
return false;
902 const uint8_t fileheader[] = {
904 0x00, 0x00, 0x00, 0x06,
909 fwrite(fileheader,
sizeof(fileheader), 1, f);
912 const uint8_t trackheader[] = {
916 fwrite(trackheader,
sizeof(trackheader), 1, f);
918 size_t tracksizepos = ftell(f) - 4;
921 uint32_t lasttime = 0;
922 size_t nexttempoindex = 0;
923 for (
size_t bi = 0; bi < this->
blocks.size(); bi++) {
927 uint32_t timediff = block.
ticktime - lasttime;
931 timediff = nexttempo.
ticktime - lasttime;
935 lasttime += timediff;
936 bool needtime =
false;
937 WriteVariableLen(f, timediff);
941 uint8_t tempobuf[6] = { MIDIST_SMF_META, 0x51, 0x03, 0, 0, 0 };
942 tempobuf[3] = (nexttempo.
tempo & 0x00FF0000) >> 16;
943 tempobuf[4] = (nexttempo.
tempo & 0x0000FF00) >> 8;
944 tempobuf[5] = (nexttempo.
tempo & 0x000000FF);
945 fwrite(tempobuf,
sizeof(tempobuf), 1, f);
958 uint8_t *dp = block.
data.data();
959 while (dp < block.
data.data() + block.
data.size()) {
967 switch (*dp & 0xF0) {
970 case MIDIST_POLYPRESS:
971 case MIDIST_CONTROLLER:
972 case MIDIST_PITCHBEND:
977 case MIDIST_CHANPRESS:
984 if (*dp == MIDIST_SYSEX) {
987 uint8_t *sysexend = dp;
988 while (*sysexend != MIDIST_ENDSYSEX) sysexend++;
989 ptrdiff_t sysexlen = sysexend - dp;
990 WriteVariableLen(f, sysexlen);
991 fwrite(dp, 1, sysexend - dp, f);
1002 static const uint8_t track_end_marker[] = { 0x00, MIDIST_SMF_META, 0x2F, 0x00 };
1003 fwrite(&track_end_marker,
sizeof(track_end_marker), 1, f);
1006 size_t trackendpos = ftell(f);
1007 fseek(f, tracksizepos, SEEK_SET);
1008 uint32_t tracksize = (uint32_t)(trackendpos - tracksizepos - 4);
1009 tracksize = TO_BE32(tracksize);
1010 fwrite(&tracksize, 4, 1, f);
1026 if (!filename.empty())
return filename;
1028 if (!filename.empty())
return filename;
1030 return std::string();
1035 char basename[MAX_PATH];
1037 const char *fnstart = strrchr(song.
filename.c_str(), PATHSEPCHAR);
1038 if (fnstart ==
nullptr) {
1045 char *wp = basename;
1046 for (
const char *rp = fnstart; *rp !=
'\0'; rp++) {
1047 if (*rp !=
'.') *wp++ = *rp;
1053 tempdirname += basename;
1057 std::string output_filename = tempdirname + std::to_string(song.
cat_index) +
".mid";
1061 return output_filename;
1065 if (!songdata.has_value())
return std::string();
1068 if (!midifile.
LoadMpsData(songdata->data(), songdata->size())) {
1069 return std::string();
1072 if (midifile.
WriteSMF(output_filename)) {
1073 return output_filename;
1075 return std::string();
1080static bool CmdDumpSMF(uint8_t argc,
char *argv[])
1083 IConsolePrint(
CC_HELP,
"Write the current song to a Standard MIDI File. Usage: 'dumpsmf <filename>'.");
1091 if (_midifile_instance ==
nullptr) {
1092 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.");
1099 if (_midifile_instance->
WriteSMF(filename)) {
1108static void RegisterConsoleMidiCommands()
1110 static bool registered =
false;
1119 RegisterConsoleMidiCommands();
1122MidiFile::~MidiFile()
1124 if (_midifile_instance ==
this) {
1125 _midifile_instance =
nullptr;
Owning byte buffer readable as a stream.
ByteBuffer(FileHandle &file, size_t len)
Construct buffer from data in a file.
bool IsValid() const
Return whether the buffer was constructed successfully.
bool ReadByte(uint8_t &b)
Read a single byte from the buffer.
bool IsEnd() const
Return whether reading has reached the end of the buffer.
bool ReadVariableLength(uint32_t &res)
Read a MIDI file variable length value.
bool ReadBuffer(uint8_t *dest, size_t length)
Read bytes into a buffer.
bool Skip(size_t count)
Skip over a number of bytes in the buffer.
bool ReadDataBlock(MidiFile::DataBlock *dest, size_t length)
Read bytes into a MidiFile::DataBlock.
bool Rewind(size_t count)
Go a number of bytes back to re-read.
void IConsolePrint(TextColour colour_code, const std::string &string)
Handle the printing of text entered into the console or redirected there by any other means.
static const TextColour CC_HELP
Colour for help lines.
static const TextColour CC_INFO
Colour for information lines.
static const TextColour CC_WARNING
Colour for warning lines.
static const TextColour CC_ERROR
Colour for error lines.
std::string FioFindFullPath(Subdirectory subdir, const std::string &filename)
Find a path to the filename in one of the search directories.
void AppendPathSeparator(std::string &buf)
Appends, if necessary, the path separator character to the end of the string.
void FioCreateDirectory(const std::string &name)
Create a directory with the given name If the parent directory does not exist, it will try to create ...
bool FileExists(const std::string &filename)
Test whether the given filename exists.
std::optional< FileHandle > FioFOpenFile(const std::string &filename, const char *mode, Subdirectory subdir, size_t *filesize)
Opens a OpenTTD file somewhere in a personal or global directory.
const char * FiosGetScreenshotDir()
Get the directory for screenshots.
@ SP_AUTODOWNLOAD_DIR
Search within the autodownload directory.
@ NO_DIRECTORY
A path without any base directory.
@ OLD_GM_DIR
Old subdirectory for the music.
@ BASESET_DIR
Subdirectory for all base data (base sets, intro game)
int MemCmpT(const T *ptr1, const T *ptr2, size_t num=1)
Type-safe version of memcmp().
#define lengthof(array)
Return the length of an fixed size array.
static void CmdRegister(const std::string &name, IConsoleCmdProc *proc, IConsoleHook *hook=nullptr)
Register a new command to be used in the console.
std::vector< uint8_t > data
raw midi data contained in block
uint32_t realtime
real-time (microseconds) since start of file this block should be triggered at
uint32_t ticktime
tick number since start of file this block should be triggered at
uint32_t tempo
new tempo in microseconds per tick
uint32_t ticktime
tick number since start of file this tempo change occurs at
std::vector< TempoChange > tempos
list of tempo changes in file
bool LoadMpsData(const uint8_t *data, size_t length)
Create MIDI data from song data for the original Microprose music drivers.
void MoveFrom(MidiFile &other)
Move data from other to this, and clears other.
bool LoadFile(const std::string &filename)
Load a standard MIDI file.
static bool ReadSMFHeader(const std::string &filename, SMFHeader &header)
Read the header of a standard MIDI file.
std::vector< DataBlock > blocks
sequential time-annotated data of file, merged to a single track
static std::string GetSMFFile(const MusicSongInfo &song)
Get the name of a Standard MIDI File for a given song.
uint16_t tickdiv
ticks per quarter note
bool WriteSMF(const std::string &filename)
Write a Standard MIDI File containing the decoded music.
Starting parameter and playback status for one channel/track.
uint8_t cur_program
program selected, used for velocity scaling (lookup into programvelocities array)
uint16_t delay
frames until next command
uint32_t playpos
next byte to play this channel from
uint8_t running_status
last midi status code seen
uint32_t startpos
start position of master track
uint32_t returnpos
next return position after playing a segment
Decoder for "MPS MIDI" format data.
MpsMachine(const uint8_t *data, size_t length, MidiFile &target)
Construct a TTD DOS music format decoder.
uint16_t PlayChannelFrame(MidiFile::DataBlock &outblock, int channel)
Play one frame of data from one channel.
const uint8_t * songdata
raw data array
int16_t initial_tempo
starting tempo of song
uint16_t ReadVariableLength(uint32_t &pos)
Read an SMF-style variable length value (note duration) from songdata.
static const uint8_t programvelocities[128]
Base note velocities for various GM programs.
int16_t tempo_ticks
ticker that increments when playing a frame, decrements before playing a frame
bool PlayFrame(MidiFile::DataBlock &block)
Play one frame of data into a block.
MidiFile & target
recipient of data
std::array< Channel, 16 > channels
playback status for each MIDI channel
int16_t current_tempo
threshold for actually playing a frame
std::vector< uint32_t > segments
pointers into songdata to repeatable data segments
size_t songdatalen
length of song data
static const int TEMPO_RATE
Frames/ticks per second for music playback.
void RestartSong()
Prepare for playback from the beginning.
MpsMidiStatus
Overridden MIDI status codes used in the data format.
@ MPSMIDIST_SEGMENT_RETURN
resume playing master track from stored position
@ MPSMIDIST_ENDSONG
immediately end the song
@ MPSMIDIST_SEGMENT_CALL
store current position of master track playback, and begin playback of a segment
bool shouldplayflag
not-end-of-song flag
bool PlayInto()
Perform playback of whole song.
Metadata about a music track.
MusicTrackType filetype
decoder required for song file
std::string filename
file on disk containing song (when used in MusicSet class)
int cat_index
entry index in CAT file, for filetype==MTT_MPSMIDI