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) {
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()) {
215 uint8_t last_status = 0;
216 bool running_sysex =
false;
217 while (!chunk.IsEnd()) {
219 uint32_t deltatime = 0;
220 if (!chunk.ReadVariableLength(deltatime)) {
229 if (!chunk.ReadByte(status)) {
233 if ((status & 0x80) == 0) {
237 status = last_status;
239 }
else if ((status & 0xF0) != 0xF0) {
241 last_status = status;
243 switch (status & 0xF0) {
246 case MIDIST_POLYPRESS:
247 case MIDIST_CONTROLLER:
248 case MIDIST_PITCHBEND:
250 block->
data.push_back(status);
251 if (!chunk.ReadDataBlock(block, 2)) {
256 case MIDIST_CHANPRESS:
258 block->
data.push_back(status);
259 if (!chunk.ReadByte(buf[0])) {
262 block->
data.push_back(buf[0]);
267 }
else if (status == MIDIST_SMF_META) {
269 if (!chunk.ReadByte(buf[0])) {
273 if (!chunk.ReadVariableLength(length)) {
279 return (length == 0);
282 if (length != 3)
return false;
283 if (!chunk.ReadBuffer(buf, 3))
return false;
288 if (!chunk.Skip(length)) {
293 }
else if (status == MIDIST_SYSEX || (status == MIDIST_SMF_ESCAPE && running_sysex)) {
296 if (!chunk.ReadVariableLength(length)) {
299 block->
data.push_back(0xF0);
300 if (!chunk.ReadDataBlock(block, length)) {
303 if (block->
data.back() != 0xF7) {
305 running_sysex =
true;
306 block->
data.push_back(0xF7);
308 running_sysex =
false;
310 }
else if (status == MIDIST_SMF_ESCAPE) {
313 if (!chunk.ReadVariableLength(length)) {
316 if (!chunk.ReadDataBlock(block, length)) {
337bool TicktimeAscending(
const T &a,
const T &b)
339 return a.ticktime < b.ticktime;
342static bool FixupMidiData(
MidiFile &target)
345 std::sort(target.
tempos.begin(), target.
tempos.end(), TicktimeAscending<MidiFile::TempoChange>);
346 std::sort(target.
blocks.begin(), target.
blocks.end(), TicktimeAscending<MidiFile::DataBlock>);
348 if (target.
tempos.empty()) {
356 std::vector<MidiFile::DataBlock> merged_blocks;
357 uint32_t last_ticktime = 0;
358 for (
size_t i = 0; i < target.
blocks.size(); i++) {
360 if (block.
data.empty()) {
362 }
else if (block.
ticktime > last_ticktime || merged_blocks.empty()) {
363 merged_blocks.push_back(block);
366 merged_blocks.back().data.insert(merged_blocks.back().data.end(), block.
data.begin(), block.
data.end());
369 std::swap(merged_blocks, target.
blocks);
373 uint32_t last_realtime = 0;
374 size_t cur_tempo = 0, cur_block = 0;
375 while (cur_block < target.
blocks.size()) {
381 int64_t tickdiff = block.
ticktime - last_ticktime;
383 last_realtime += uint32_t(tickdiff * tempo.
tempo / target.
tickdiv);
388 int64_t tickdiff = next_tempo.
ticktime - last_ticktime;
389 last_ticktime = next_tempo.
ticktime;
390 last_realtime += uint32_t(tickdiff * tempo.
tempo / target.
tickdiv);
407 if (!file.has_value())
return false;
423 if (fread(buffer,
sizeof(buffer), 1, file) != 1) {
428 const uint8_t magic[] = {
'M',
'T',
'h',
'd', 0x00, 0x00, 0x00, 0x06 };
429 if (
MemCmpT(buffer, magic,
sizeof(magic)) != 0) {
434 header.format = (buffer[8] << 8) | buffer[9];
435 header.tracks = (buffer[10] << 8) | buffer[11];
436 header.tickdiv = (buffer[12] << 8) | buffer[13];
447 _midifile_instance =
this;
454 if (!file.has_value())
return false;
460 if (header.format != 0 && header.format != 1)
return false;
462 if ((header.tickdiv & 0x8000) != 0)
return false;
464 this->
tickdiv = header.tickdiv;
466 for (; header.tracks > 0; header.tracks--) {
467 if (!ReadTrackChunk(*file, *
this)) {
472 return FixupMidiData(*
this);
531 block.
data.push_back(b1);
532 block.
data.push_back(b2);
536 block.
data.push_back(b1);
537 block.
data.push_back(b2);
538 block.
data.push_back(b3);
555 this->initial_tempo = this->songdata[pos++];
558 loopmax = this->songdata[pos++];
559 for (loopidx = 0; loopidx < loopmax; loopidx++) {
564 this->segments.push_back(pos + 4);
565 pos += FROM_LE16(*(
const int16_t *)(this->songdata + pos));
570 loopmax = this->songdata[pos++];
571 for (loopidx = 0; loopidx < loopmax; loopidx++) {
575 uint8_t ch = this->songdata[pos++];
576 this->channels[ch].
startpos = pos + 4;
577 pos += FROM_LE16(*(
const int16_t *)(this->songdata + pos));
591 b = this->songdata[pos++];
592 res = (res << 7) + (b & 0x7F);
602 for (
int ch = 0; ch < 16; ch++) {
603 Channel &chandata = this->channels[ch];
621 uint16_t newdelay = 0;
623 Channel &chandata = this->channels[channel];
627 b1 = this->songdata[chandata.
playpos++];
631 b1 = this->songdata[chandata.
playpos++];
633 chandata.
playpos = this->segments[b1];
654 this->shouldplayflag =
false;
663 b1 = this->songdata[chandata.
playpos++];
669 b2 = this->songdata[chandata.
playpos++];
675 velocity = (int16_t)b2 * 0x50;
680 b2 = (velocity / 128) & 0x00FF;
681 AddMidiData(outblock, MIDIST_NOTEON + channel, b1, b2);
684 AddMidiData(outblock, MIDIST_NOTEON + channel, b1, 0);
687 case MIDIST_CONTROLLER:
688 b2 = this->songdata[chandata.
playpos++];
689 if (b1 == MIDICT_MODE_MONO) {
695 }
else if (b1 == 0) {
699 this->current_tempo = ((int)b2) * 48 / 60;
702 }
else if (b1 == MIDICT_EFFECTS1) {
707 AddMidiData(outblock, MIDIST_CONTROLLER + channel, b1, b2);
715 this->shouldplayflag =
false;
722 if (b1 == 0x57 || b1 == 0x3F) {
725 AddMidiData(outblock, MIDIST_PROGCHG + channel, b1);
727 case MIDIST_PITCHBEND:
728 b2 = this->songdata[chandata.
playpos++];
729 AddMidiData(outblock, MIDIST_PITCHBEND + channel, b1, b2);
736 }
while (newdelay == 0);
748 if (this->tempo_ticks > 0) {
754 for (
int ch = 0; ch < 16; ch++) {
755 Channel &chandata = this->channels[ch];
757 if (chandata.
delay == 0) {
781 this->shouldplayflag =
true;
782 this->current_tempo = (int32_t)this->initial_tempo * 24 / 60;
787 AddMidiData(this->target.
blocks.back(), MIDIST_PROGCHG + 9, 0x00);
792 for (uint32_t tick = 0; tick < 100000; tick += 1) {
793 auto &block = this->target.
blocks.emplace_back();
806 100, 100, 100, 100, 100, 90, 100, 100, 100, 100, 100, 90, 100, 100, 100, 100,
807 100, 100, 85, 100, 100, 100, 100, 100, 100, 100, 100, 100, 90, 90, 110, 80,
808 100, 100, 100, 90, 70, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
809 100, 100, 90, 100, 100, 100, 100, 100, 100, 120, 100, 100, 100, 120, 100, 127,
810 100, 100, 90, 100, 100, 100, 100, 100, 100, 95, 100, 100, 100, 100, 100, 100,
811 100, 100, 100, 100, 100, 100, 100, 115, 100, 100, 100, 100, 100, 100, 100, 100,
812 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
813 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
824 _midifile_instance =
this;
827 return machine.
PlayInto() && FixupMidiData(*
this);
838 if (songdata.has_value()) {
839 bool result = this->
LoadMpsData(songdata->data(), songdata->size());
860 _midifile_instance =
this;
867static void WriteVariableLen(
FileHandle &f, uint32_t value)
871 fwrite(&tb, 1, 1, f);
872 }
else if (value <= 0x3FFF) {
874 tb[1] = value & 0x7F; value >>= 7;
875 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
876 fwrite(tb, 1,
sizeof(tb), f);
877 }
else if (value <= 0x1FFFFF) {
879 tb[2] = value & 0x7F; value >>= 7;
880 tb[1] = (value & 0x7F) | 0x80; value >>= 7;
881 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
882 fwrite(tb, 1,
sizeof(tb), f);
883 }
else if (value <= 0x0FFFFFFF) {
885 tb[3] = value & 0x7F; value >>= 7;
886 tb[2] = (value & 0x7F) | 0x80; value >>= 7;
887 tb[1] = (value & 0x7F) | 0x80; value >>= 7;
888 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
889 fwrite(tb, 1,
sizeof(tb), f);
901 if (!of.has_value())
return false;
905 const uint8_t fileheader[] = {
907 0x00, 0x00, 0x00, 0x06,
912 fwrite(fileheader,
sizeof(fileheader), 1, f);
915 const uint8_t trackheader[] = {
919 fwrite(trackheader,
sizeof(trackheader), 1, f);
921 size_t tracksizepos = ftell(f) - 4;
924 uint32_t lasttime = 0;
925 size_t nexttempoindex = 0;
926 for (
size_t bi = 0; bi < this->
blocks.size(); bi++) {
930 uint32_t timediff = block.
ticktime - lasttime;
934 timediff = nexttempo.
ticktime - lasttime;
938 lasttime += timediff;
939 bool needtime =
false;
940 WriteVariableLen(f, timediff);
944 uint8_t tempobuf[6] = { MIDIST_SMF_META, 0x51, 0x03, 0, 0, 0 };
945 tempobuf[3] = (nexttempo.
tempo & 0x00FF0000) >> 16;
946 tempobuf[4] = (nexttempo.
tempo & 0x0000FF00) >> 8;
947 tempobuf[5] = (nexttempo.
tempo & 0x000000FF);
948 fwrite(tempobuf,
sizeof(tempobuf), 1, f);
961 uint8_t *dp = block.
data.data();
962 while (dp < block.
data.data() + block.
data.size()) {
970 switch (*dp & 0xF0) {
973 case MIDIST_POLYPRESS:
974 case MIDIST_CONTROLLER:
975 case MIDIST_PITCHBEND:
980 case MIDIST_CHANPRESS:
987 if (*dp == MIDIST_SYSEX) {
990 uint8_t *sysexend = dp;
991 while (*sysexend != MIDIST_ENDSYSEX) sysexend++;
992 ptrdiff_t sysexlen = sysexend - dp;
993 WriteVariableLen(f, sysexlen);
994 fwrite(dp, 1, sysexend - dp, f);
1005 static const uint8_t track_end_marker[] = { 0x00, MIDIST_SMF_META, 0x2F, 0x00 };
1006 fwrite(&track_end_marker,
sizeof(track_end_marker), 1, f);
1009 size_t trackendpos = ftell(f);
1010 fseek(f, tracksizepos, SEEK_SET);
1011 uint32_t tracksize = (uint32_t)(trackendpos - tracksizepos - 4);
1012 tracksize = TO_BE32(tracksize);
1013 fwrite(&tracksize, 4, 1, f);
1029 if (!filename.empty())
return filename;
1031 if (!filename.empty())
return filename;
1033 return std::string();
1038 char basename[MAX_PATH];
1040 const char *fnstart = strrchr(song.
filename.c_str(), PATHSEPCHAR);
1041 if (fnstart ==
nullptr) {
1048 char *wp = basename;
1049 for (
const char *rp = fnstart; *rp !=
'\0'; rp++) {
1050 if (*rp !=
'.') *wp++ = *rp;
1056 tempdirname += basename;
1060 std::string output_filename = tempdirname + std::to_string(song.
cat_index) +
".mid";
1064 return output_filename;
1068 if (!songdata.has_value())
return std::string();
1071 if (!midifile.
LoadMpsData(songdata->data(), songdata->size())) {
1072 return std::string();
1075 if (midifile.
WriteSMF(output_filename)) {
1076 return output_filename;
1078 return std::string();
1083static bool CmdDumpSMF(uint8_t argc,
char *argv[])
1086 IConsolePrint(
CC_HELP,
"Write the current song to a Standard MIDI File. Usage: 'dumpsmf <filename>'.");
1094 if (_midifile_instance ==
nullptr) {
1095 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.");
1102 if (_midifile_instance->
WriteSMF(filename)) {
1111static void RegisterConsoleMidiCommands()
1113 static bool registered =
false;
1122 RegisterConsoleMidiCommands();
1125MidiFile::~MidiFile()
1127 if (_midifile_instance ==
this) {
1128 _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