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"
25 static MidiFile *_midifile_instance =
nullptr;
33 const 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) {
90 return !this->buf.empty();
99 return this->pos >= this->buf.size();
109 if (this->
IsEnd())
return false;
110 b = this->buf[this->pos++];
126 if (this->
IsEnd())
return false;
127 b = this->buf[this->pos++];
128 res = (res << 7) | (b & 0x7F);
141 if (this->
IsEnd())
return false;
142 if (this->buf.size() - this->pos < length)
return false;
143 std::copy(std::begin(this->buf) + this->pos, std::begin(this->buf) + this->pos + length, dest);
156 if (this->
IsEnd())
return false;
157 if (this->buf.size() - this->pos < length)
return false;
158 dest->
data.insert(dest->
data.end(), std::begin(this->buf) + this->pos, std::begin(this->buf) + this->pos + length);
170 if (this->
IsEnd())
return false;
171 if (this->buf.size() - this->pos < count)
return false;
183 if (count > this->pos)
return false;
193 const uint8_t magic[] = {
'M',
'T',
'r',
'k' };
194 if (fread(buf,
sizeof(magic), 1, file) != 1) {
197 if (memcmp(magic, buf,
sizeof(magic)) != 0) {
202 uint32_t chunk_length;
203 if (fread(&chunk_length, 1, 4, file) != 4) {
206 chunk_length = FROM_BE32(chunk_length);
209 if (!chunk.IsValid()) {
216 uint8_t last_status = 0;
217 bool running_sysex =
false;
218 while (!chunk.IsEnd()) {
220 uint32_t deltatime = 0;
221 if (!chunk.ReadVariableLength(deltatime)) {
226 block = &target.
blocks.back();
231 if (!chunk.ReadByte(status)) {
235 if ((status & 0x80) == 0) {
239 status = last_status;
241 }
else if ((status & 0xF0) != 0xF0) {
243 last_status = status;
245 switch (status & 0xF0) {
248 case MIDIST_POLYPRESS:
249 case MIDIST_CONTROLLER:
250 case MIDIST_PITCHBEND:
252 block->
data.push_back(status);
253 if (!chunk.ReadDataBlock(block, 2)) {
258 case MIDIST_CHANPRESS:
260 block->
data.push_back(status);
261 if (!chunk.ReadByte(buf[0])) {
264 block->
data.push_back(buf[0]);
269 }
else if (status == MIDIST_SMF_META) {
271 if (!chunk.ReadByte(buf[0])) {
275 if (!chunk.ReadVariableLength(length)) {
281 return (length == 0);
284 if (length != 3)
return false;
285 if (!chunk.ReadBuffer(buf, 3))
return false;
290 if (!chunk.Skip(length)) {
295 }
else if (status == MIDIST_SYSEX || (status == MIDIST_SMF_ESCAPE && running_sysex)) {
298 if (!chunk.ReadVariableLength(length)) {
301 block->
data.push_back(0xF0);
302 if (!chunk.ReadDataBlock(block, length)) {
305 if (block->
data.back() != 0xF7) {
307 running_sysex =
true;
308 block->
data.push_back(0xF7);
310 running_sysex =
false;
312 }
else if (status == MIDIST_SMF_ESCAPE) {
315 if (!chunk.ReadVariableLength(length)) {
318 if (!chunk.ReadDataBlock(block, length)) {
339 bool TicktimeAscending(
const T &a,
const T &b)
341 return a.ticktime < b.ticktime;
344 static bool FixupMidiData(
MidiFile &target)
347 std::sort(target.
tempos.begin(), target.
tempos.end(), TicktimeAscending<MidiFile::TempoChange>);
348 std::sort(target.
blocks.begin(), target.
blocks.end(), TicktimeAscending<MidiFile::DataBlock>);
350 if (target.
tempos.empty()) {
358 std::vector<MidiFile::DataBlock> merged_blocks;
359 uint32_t last_ticktime = 0;
360 for (
size_t i = 0; i < target.
blocks.size(); i++) {
362 if (block.
data.empty()) {
364 }
else if (block.
ticktime > last_ticktime || merged_blocks.empty()) {
365 merged_blocks.push_back(block);
368 merged_blocks.back().data.insert(merged_blocks.back().data.end(), block.
data.begin(), block.
data.end());
371 std::swap(merged_blocks, target.
blocks);
375 uint32_t last_realtime = 0;
376 size_t cur_tempo = 0, cur_block = 0;
377 while (cur_block < target.
blocks.size()) {
383 int64_t tickdiff = block.
ticktime - last_ticktime;
385 last_realtime += uint32_t(tickdiff * tempo.
tempo / target.
tickdiv);
390 int64_t tickdiff = next_tempo.
ticktime - last_ticktime;
391 last_ticktime = next_tempo.
ticktime;
392 last_realtime += uint32_t(tickdiff * tempo.
tempo / target.
tickdiv);
409 if (!file.has_value())
return false;
425 if (fread(buffer,
sizeof(buffer), 1, file) != 1) {
430 const uint8_t magic[] = {
'M',
'T',
'h',
'd', 0x00, 0x00, 0x00, 0x06 };
431 if (
MemCmpT(buffer, magic,
sizeof(magic)) != 0) {
436 header.format = (buffer[8] << 8) | buffer[9];
437 header.tracks = (buffer[10] << 8) | buffer[11];
438 header.tickdiv = (buffer[12] << 8) | buffer[13];
449 _midifile_instance =
this;
456 if (!file.has_value())
return false;
462 if (header.format != 0 && header.format != 1)
return false;
464 if ((header.tickdiv & 0x8000) != 0)
return false;
466 this->
tickdiv = header.tickdiv;
468 for (; header.tracks > 0; header.tracks--) {
469 if (!ReadTrackChunk(*file, *
this)) {
474 return FixupMidiData(*
this);
533 block.
data.push_back(b1);
534 block.
data.push_back(b2);
538 block.
data.push_back(b1);
539 block.
data.push_back(b2);
540 block.
data.push_back(b3);
557 this->initial_tempo = this->songdata[pos++];
560 loopmax = this->songdata[pos++];
561 for (loopidx = 0; loopidx < loopmax; loopidx++) {
566 this->segments.push_back(pos + 4);
567 pos += FROM_LE16(*(
const int16_t *)(this->songdata + pos));
572 loopmax = this->songdata[pos++];
573 for (loopidx = 0; loopidx < loopmax; loopidx++) {
577 uint8_t ch = this->songdata[pos++];
578 this->channels[ch].
startpos = pos + 4;
579 pos += FROM_LE16(*(
const int16_t *)(this->songdata + pos));
593 b = this->songdata[pos++];
594 res = (res << 7) + (b & 0x7F);
604 for (
int ch = 0; ch < 16; ch++) {
605 Channel &chandata = this->channels[ch];
623 uint16_t newdelay = 0;
625 Channel &chandata = this->channels[channel];
629 b1 = this->songdata[chandata.
playpos++];
633 b1 = this->songdata[chandata.
playpos++];
635 chandata.
playpos = this->segments[b1];
656 this->shouldplayflag =
false;
665 b1 = this->songdata[chandata.
playpos++];
671 b2 = this->songdata[chandata.
playpos++];
677 velocity = (int16_t)b2 * 0x50;
682 b2 = (velocity / 128) & 0x00FF;
683 AddMidiData(outblock, MIDIST_NOTEON + channel, b1, b2);
686 AddMidiData(outblock, MIDIST_NOTEON + channel, b1, 0);
689 case MIDIST_CONTROLLER:
690 b2 = this->songdata[chandata.
playpos++];
691 if (b1 == MIDICT_MODE_MONO) {
697 }
else if (b1 == 0) {
701 this->current_tempo = ((int)b2) * 48 / 60;
704 }
else if (b1 == MIDICT_EFFECTS1) {
709 AddMidiData(outblock, MIDIST_CONTROLLER + channel, b1, b2);
717 this->shouldplayflag =
false;
724 if (b1 == 0x57 || b1 == 0x3F) {
727 AddMidiData(outblock, MIDIST_PROGCHG + channel, b1);
729 case MIDIST_PITCHBEND:
730 b2 = this->songdata[chandata.
playpos++];
731 AddMidiData(outblock, MIDIST_PITCHBEND + channel, b1, b2);
738 }
while (newdelay == 0);
750 if (this->tempo_ticks > 0) {
756 for (
int ch = 0; ch < 16; ch++) {
757 Channel &chandata = this->channels[ch];
759 if (chandata.
delay == 0) {
783 this->shouldplayflag =
true;
784 this->current_tempo = (int32_t)this->initial_tempo * 24 / 60;
789 AddMidiData(this->target.
blocks.back(), MIDIST_PROGCHG + 9, 0x00);
794 for (uint32_t tick = 0; tick < 100000; tick += 1) {
796 auto &block = this->target.
blocks.back();
809 100, 100, 100, 100, 100, 90, 100, 100, 100, 100, 100, 90, 100, 100, 100, 100,
810 100, 100, 85, 100, 100, 100, 100, 100, 100, 100, 100, 100, 90, 90, 110, 80,
811 100, 100, 100, 90, 70, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
812 100, 100, 90, 100, 100, 100, 100, 100, 100, 120, 100, 100, 100, 120, 100, 127,
813 100, 100, 90, 100, 100, 100, 100, 100, 100, 95, 100, 100, 100, 100, 100, 100,
814 100, 100, 100, 100, 100, 100, 100, 115, 100, 100, 100, 100, 100, 100, 100, 100,
815 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
816 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
827 _midifile_instance =
this;
830 return machine.
PlayInto() && FixupMidiData(*
this);
841 if (songdata.has_value()) {
842 bool result = this->
LoadMpsData(songdata->data(), songdata->size());
863 _midifile_instance =
this;
870 static void WriteVariableLen(
FileHandle &f, uint32_t value)
874 fwrite(&tb, 1, 1, f);
875 }
else if (value <= 0x3FFF) {
877 tb[1] = value & 0x7F; value >>= 7;
878 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
879 fwrite(tb, 1,
sizeof(tb), f);
880 }
else if (value <= 0x1FFFFF) {
882 tb[2] = value & 0x7F; value >>= 7;
883 tb[1] = (value & 0x7F) | 0x80; value >>= 7;
884 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
885 fwrite(tb, 1,
sizeof(tb), f);
886 }
else if (value <= 0x0FFFFFFF) {
888 tb[3] = value & 0x7F; value >>= 7;
889 tb[2] = (value & 0x7F) | 0x80; value >>= 7;
890 tb[1] = (value & 0x7F) | 0x80; value >>= 7;
891 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
892 fwrite(tb, 1,
sizeof(tb), f);
904 if (!of.has_value())
return false;
908 const uint8_t fileheader[] = {
910 0x00, 0x00, 0x00, 0x06,
915 fwrite(fileheader,
sizeof(fileheader), 1, f);
918 const uint8_t trackheader[] = {
922 fwrite(trackheader,
sizeof(trackheader), 1, f);
924 size_t tracksizepos = ftell(f) - 4;
927 uint32_t lasttime = 0;
928 size_t nexttempoindex = 0;
929 for (
size_t bi = 0; bi < this->
blocks.size(); bi++) {
933 uint32_t timediff = block.
ticktime - lasttime;
937 timediff = nexttempo.
ticktime - lasttime;
941 lasttime += timediff;
942 bool needtime =
false;
943 WriteVariableLen(f, timediff);
947 uint8_t tempobuf[6] = { MIDIST_SMF_META, 0x51, 0x03, 0, 0, 0 };
948 tempobuf[3] = (nexttempo.
tempo & 0x00FF0000) >> 16;
949 tempobuf[4] = (nexttempo.
tempo & 0x0000FF00) >> 8;
950 tempobuf[5] = (nexttempo.
tempo & 0x000000FF);
951 fwrite(tempobuf,
sizeof(tempobuf), 1, f);
964 uint8_t *dp = block.
data.data();
965 while (dp < block.
data.data() + block.
data.size()) {
973 switch (*dp & 0xF0) {
976 case MIDIST_POLYPRESS:
977 case MIDIST_CONTROLLER:
978 case MIDIST_PITCHBEND:
983 case MIDIST_CHANPRESS:
990 if (*dp == MIDIST_SYSEX) {
993 uint8_t *sysexend = dp;
994 while (*sysexend != MIDIST_ENDSYSEX) sysexend++;
995 ptrdiff_t sysexlen = sysexend - dp;
996 WriteVariableLen(f, sysexlen);
997 fwrite(dp, 1, sysexend - dp, f);
1008 static const uint8_t track_end_marker[] = { 0x00, MIDIST_SMF_META, 0x2F, 0x00 };
1009 fwrite(&track_end_marker,
sizeof(track_end_marker), 1, f);
1012 size_t trackendpos = ftell(f);
1013 fseek(f, tracksizepos, SEEK_SET);
1014 uint32_t tracksize = (uint32_t)(trackendpos - tracksizepos - 4);
1015 tracksize = TO_BE32(tracksize);
1016 fwrite(&tracksize, 4, 1, f);
1032 if (!filename.empty())
return filename;
1034 if (!filename.empty())
return filename;
1036 return std::string();
1041 char basename[MAX_PATH];
1043 const char *fnstart = strrchr(song.
filename.c_str(), PATHSEPCHAR);
1044 if (fnstart ==
nullptr) {
1051 char *wp = basename;
1052 for (
const char *rp = fnstart; *rp !=
'\0'; rp++) {
1053 if (*rp !=
'.') *wp++ = *rp;
1059 tempdirname += basename;
1063 std::string output_filename = tempdirname + std::to_string(song.
cat_index) +
".mid";
1067 return output_filename;
1071 if (!songdata.has_value())
return std::string();
1074 if (!midifile.
LoadMpsData(songdata->data(), songdata->size())) {
1075 return std::string();
1078 if (midifile.
WriteSMF(output_filename)) {
1079 return output_filename;
1081 return std::string();
1086 static bool CmdDumpSMF(uint8_t argc,
char *argv[])
1089 IConsolePrint(
CC_HELP,
"Write the current song to a Standard MIDI File. Usage: 'dumpsmf <filename>'.");
1097 if (_midifile_instance ==
nullptr) {
1098 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.");
1105 if (_midifile_instance->
WriteSMF(filename)) {
1114 static void RegisterConsoleMidiCommands()
1116 static bool registered =
false;
1123 MidiFile::MidiFile()
1125 RegisterConsoleMidiCommands();
1128 MidiFile::~MidiFile()
1130 if (_midifile_instance ==
this) {
1131 _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.
Channel channels[16]
playback status for each MIDI channel
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
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.
bool shouldplayflag
not-end-of-song flag
bool PlayInto()
Perform playback of whole song.
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
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