24#include "table/strings.h"
31static MidiFile *_midifile_instance =
nullptr;
41 static uint8_t reset_gm_sysex[] = { 0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7 };
42 static uint8_t reset_gs_sysex[] = { 0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7 };
43 static uint8_t reset_xg_sysex[] = { 0xF0, 0x43, 0x10, 0x4C, 0x00, 0x00, 0x7E, 0x00, 0xF7 };
44 static uint8_t roland_reverb_sysex[] = { 0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x01, 0x30, 0x02, 0x04, 0x00, 0x40, 0x40, 0x00, 0x00, 0x09, 0xF7 };
49 return reset_gm_sysex;
52 return reset_gs_sysex;
55 return reset_xg_sysex;
57 length =
lengthof(roland_reverb_sysex);
58 return roland_reverb_sysex;
69 std::vector<uint8_t> buf{};
81 this->buf.resize(len);
82 if (fread(this->buf.data(), 1, len, file) != len) {
94 return !this->buf.empty();
103 return this->pos >= this->buf.size();
113 if (this->
IsEnd())
return false;
114 b = this->buf[this->pos++];
130 if (this->
IsEnd())
return false;
131 b = this->buf[this->pos++];
132 res = (res << 7) | (b & 0x7F);
145 if (this->
IsEnd())
return false;
146 if (this->buf.size() - this->pos < length)
return false;
147 std::copy(std::begin(this->buf) + this->pos, std::begin(this->buf) + this->pos + length, dest);
160 if (this->
IsEnd())
return false;
161 if (this->buf.size() - this->pos < length)
return false;
162 dest->
data.insert(dest->
data.end(), std::begin(this->buf) + this->pos, std::begin(this->buf) + this->pos + length);
174 if (this->
IsEnd())
return false;
175 if (this->buf.size() - this->pos < count)
return false;
187 if (count > this->pos)
return false;
197 const uint8_t magic[] = {
'M',
'T',
'r',
'k' };
198 if (fread(buf,
sizeof(magic), 1, file) != 1) {
201 if (!std::ranges::equal(magic, buf)) {
206 uint32_t chunk_length;
207 if (fread(&chunk_length, 1, 4, file) != 4) {
210 chunk_length = FROM_BE32(chunk_length);
213 if (chunk_length > 1024 * 1024)
return false;
216 if (!chunk.IsValid()) {
222 uint8_t last_status = 0;
223 bool running_sysex =
false;
224 while (!chunk.IsEnd()) {
226 uint32_t deltatime = 0;
227 if (!chunk.ReadVariableLength(deltatime)) {
236 if (!chunk.ReadByte(status)) {
240 if ((status & 0x80) == 0) {
244 status = last_status;
246 }
else if ((status & 0xF0) != 0xF0) {
248 last_status = status;
250 switch (status & 0xF0) {
253 case MIDIST_POLYPRESS:
254 case MIDIST_CONTROLLER:
255 case MIDIST_PITCHBEND:
257 block->
data.push_back(status);
258 if (!chunk.ReadDataBlock(block, 2)) {
263 case MIDIST_CHANPRESS:
265 block->
data.push_back(status);
266 if (!chunk.ReadByte(buf[0])) {
269 block->
data.push_back(buf[0]);
276 if (!chunk.ReadByte(buf[0])) {
280 if (!chunk.ReadVariableLength(length)) {
286 return (length == 0);
289 if (length != 3)
return false;
290 if (!chunk.ReadBuffer(buf, 3))
return false;
295 if (!chunk.Skip(length)) {
300 }
else if (status == MIDIST_SYSEX || (status ==
MIDIST_SMF_ESCAPE && running_sysex)) {
303 if (!chunk.ReadVariableLength(length)) {
306 block->
data.push_back(0xF0);
307 if (!chunk.ReadDataBlock(block, length)) {
310 if (block->
data.back() != 0xF7) {
312 running_sysex =
true;
313 block->
data.push_back(0xF7);
315 running_sysex =
false;
320 if (!chunk.ReadVariableLength(length)) {
323 if (!chunk.ReadDataBlock(block, length)) {
344bool TicktimeAscending(
const T &a,
const T &b)
346 return a.ticktime < b.ticktime;
349static bool FixupMidiData(
MidiFile &target)
352 std::sort(target.
tempos.begin(), target.
tempos.end(), TicktimeAscending<MidiFile::TempoChange>);
353 std::sort(target.
blocks.begin(), target.
blocks.end(), TicktimeAscending<MidiFile::DataBlock>);
355 if (target.
tempos.empty()) {
363 std::vector<MidiFile::DataBlock> merged_blocks;
364 uint32_t last_ticktime = 0;
365 for (
size_t i = 0; i < target.
blocks.size(); i++) {
367 if (block.
data.empty()) {
369 }
else if (block.
ticktime > last_ticktime || merged_blocks.empty()) {
370 merged_blocks.push_back(block);
373 merged_blocks.back().data.insert(merged_blocks.back().data.end(), block.
data.begin(), block.
data.end());
376 std::swap(merged_blocks, target.
blocks);
380 int64_t last_realtime = 0;
381 size_t cur_tempo = 0, cur_block = 0;
382 while (cur_block < target.
blocks.size()) {
388 int64_t tickdiff = block.
ticktime - last_ticktime;
390 last_realtime += tickdiff * tempo.
tempo / target.
tickdiv;
395 int64_t tickdiff = next_tempo.
ticktime - last_ticktime;
396 last_ticktime = next_tempo.
ticktime;
397 last_realtime += tickdiff * tempo.
tempo / target.
tickdiv;
414 if (!file.has_value())
return false;
430 if (fread(buffer,
sizeof(buffer), 1, file) != 1) {
435 const uint8_t magic[] = {
'M',
'T',
'h',
'd', 0x00, 0x00, 0x00, 0x06 };
436 if (!std::ranges::equal(std::span(buffer, std::size(magic)), magic)) {
441 header.format = (buffer[8] << 8) | buffer[9];
442 header.tracks = (buffer[10] << 8) | buffer[11];
443 header.tickdiv = (buffer[12] << 8) | buffer[13];
454 _midifile_instance =
this;
461 if (!file.has_value())
return false;
467 if (header.format != 0 && header.format != 1)
return false;
469 if ((header.tickdiv & 0x8000) != 0)
return false;
471 if (header.tickdiv == 0)
return false;
473 this->
tickdiv = header.tickdiv;
475 for (; header.tracks > 0; header.tracks--) {
476 if (!ReadTrackChunk(*file, *
this)) {
481 return FixupMidiData(*
this);
539 block.
data.push_back(b1);
540 block.
data.push_back(b2);
542 static void AddMidiData(MidiFile::DataBlock &block, uint8_t b1, uint8_t b2, uint8_t b3)
544 block.
data.push_back(b1);
545 block.
data.push_back(b2);
546 block.
data.push_back(b3);
563 this->initial_tempo = this->songdata[pos++];
566 loopmax = this->songdata[pos++];
567 for (loopidx = 0; loopidx < loopmax; loopidx++) {
572 this->segments.push_back(pos + 4);
573 pos += FROM_LE16(*(
const int16_t *)(this->songdata + pos));
578 loopmax = this->songdata[pos++];
579 for (loopidx = 0; loopidx < loopmax; loopidx++) {
583 uint8_t ch = this->songdata[pos++];
584 this->channels[ch].startpos = pos + 4;
585 pos += FROM_LE16(*(
const int16_t *)(this->songdata + pos));
599 b = this->songdata[pos++];
600 res = (res << 7) + (b & 0x7F);
610 for (
int ch = 0; ch < 16; ch++) {
611 Channel &chandata = this->channels[ch];
632 uint16_t newdelay = 0;
634 Channel &chandata = this->channels[channel];
638 b1 = this->songdata[chandata.
playpos++];
642 b1 = this->songdata[chandata.
playpos++];
644 chandata.
playpos = this->segments[b1];
665 this->shouldplayflag =
false;
674 b1 = this->songdata[chandata.
playpos++];
680 b2 = this->songdata[chandata.
playpos++];
686 velocity = (int16_t)b2 * 0x50;
691 b2 = (velocity / 128) & 0x00FF;
692 AddMidiData(outblock, MIDIST_NOTEON + channel, b1, b2);
695 AddMidiData(outblock, MIDIST_NOTEON + channel, b1, 0);
698 case MIDIST_CONTROLLER:
699 b2 = this->songdata[chandata.
playpos++];
700 if (b1 == MIDICT_MODE_MONO) {
706 }
else if (b1 == 0) {
710 this->current_tempo = ((int)b2) * 48 / 60;
713 }
else if (b1 == MIDICT_EFFECTS1) {
718 AddMidiData(outblock, MIDIST_CONTROLLER + channel, b1, b2);
726 this->shouldplayflag =
false;
733 if (b1 == 0x57 || b1 == 0x3F) {
736 AddMidiData(outblock, MIDIST_PROGCHG + channel, b1);
738 case MIDIST_PITCHBEND:
739 b2 = this->songdata[chandata.
playpos++];
740 AddMidiData(outblock, MIDIST_PITCHBEND + channel, b1, b2);
747 }
while (newdelay == 0);
760 this->tempo_ticks -= this->current_tempo;
761 if (this->tempo_ticks > 0) {
767 for (
int ch = 0; ch < 16; ch++) {
768 Channel &chandata = this->channels[ch];
770 if (chandata.
delay == 0) {
777 return this->shouldplayflag;
795 this->shouldplayflag =
true;
796 this->current_tempo = (int32_t)this->initial_tempo * 24 / 60;
797 this->tempo_ticks = this->current_tempo;
800 auto &data_block = this->target.
blocks.emplace_back();
801 AddMidiData(data_block, MIDIST_PROGCHG + 9, 0x00);
806 for (uint32_t tick = 0; tick < 100000; tick += 1) {
807 auto &block = this->target.
blocks.emplace_back();
820 100, 100, 100, 100, 100, 90, 100, 100, 100, 100, 100, 90, 100, 100, 100, 100,
821 100, 100, 85, 100, 100, 100, 100, 100, 100, 100, 100, 100, 90, 90, 110, 80,
822 100, 100, 100, 90, 70, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
823 100, 100, 90, 100, 100, 100, 100, 100, 100, 120, 100, 100, 100, 120, 100, 127,
824 100, 100, 90, 100, 100, 100, 100, 100, 100, 95, 100, 100, 100, 100, 100, 100,
825 100, 100, 100, 100, 100, 100, 100, 115, 100, 100, 100, 100, 100, 100, 100, 100,
826 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
827 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
838 _midifile_instance =
this;
841 return machine.
PlayInto() && FixupMidiData(*
this);
852 if (songdata.has_value()) {
853 bool result = this->
LoadMpsData(songdata->data(), songdata->size());
874 _midifile_instance =
this;
881static void WriteVariableLen(
FileHandle &f, uint32_t value)
885 fwrite(&tb, 1, 1, f);
886 }
else if (value <= 0x3FFF) {
888 tb[1] = value & 0x7F; value >>= 7;
889 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
890 fwrite(tb, 1,
sizeof(tb), f);
891 }
else if (value <= 0x1FFFFF) {
893 tb[2] = value & 0x7F; value >>= 7;
894 tb[1] = (value & 0x7F) | 0x80; value >>= 7;
895 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
896 fwrite(tb, 1,
sizeof(tb), f);
897 }
else if (value <= 0x0FFFFFFF) {
899 tb[3] = value & 0x7F; value >>= 7;
900 tb[2] = (value & 0x7F) | 0x80; value >>= 7;
901 tb[1] = (value & 0x7F) | 0x80; value >>= 7;
902 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
903 fwrite(tb, 1,
sizeof(tb), f);
915 if (!of.has_value())
return false;
919 const uint8_t fileheader[] = {
921 0x00, 0x00, 0x00, 0x06,
926 fwrite(fileheader,
sizeof(fileheader), 1, f);
929 const uint8_t trackheader[] = {
933 fwrite(trackheader,
sizeof(trackheader), 1, f);
935 size_t tracksizepos = ftell(f) - 4;
938 uint32_t lasttime = 0;
939 size_t nexttempoindex = 0;
940 for (
size_t bi = 0; bi < this->
blocks.size(); bi++) {
944 uint32_t timediff = block.
ticktime - lasttime;
948 timediff = nexttempo.
ticktime - lasttime;
952 lasttime += timediff;
953 bool needtime =
false;
954 WriteVariableLen(f, timediff);
959 tempobuf[3] = (nexttempo.
tempo & 0x00FF0000) >> 16;
960 tempobuf[4] = (nexttempo.
tempo & 0x0000FF00) >> 8;
961 tempobuf[5] = (nexttempo.
tempo & 0x000000FF);
962 fwrite(tempobuf,
sizeof(tempobuf), 1, f);
975 uint8_t *dp = block.
data.data();
976 while (dp < block.
data.data() + block.
data.size()) {
984 switch (*dp & 0xF0) {
987 case MIDIST_POLYPRESS:
988 case MIDIST_CONTROLLER:
989 case MIDIST_PITCHBEND:
994 case MIDIST_CHANPRESS:
1001 if (*dp == MIDIST_SYSEX) {
1002 fwrite(dp, 1, 1, f);
1004 uint8_t *sysexend = dp;
1006 ptrdiff_t sysexlen = sysexend - dp;
1007 WriteVariableLen(f, sysexlen);
1008 fwrite(dp, 1, sysexend - dp, f);
1019 static const uint8_t track_end_marker[] = { 0x00,
MIDIST_SMF_META, 0x2F, 0x00 };
1020 fwrite(&track_end_marker,
sizeof(track_end_marker), 1, f);
1023 size_t trackendpos = ftell(f);
1024 fseek(f, tracksizepos, SEEK_SET);
1025 uint32_t tracksize = (uint32_t)(trackendpos - tracksizepos - 4);
1026 tracksize = TO_BE32(tracksize);
1027 fwrite(&tracksize, 4, 1, f);
1043 if (!filename.empty())
return filename;
1045 if (!filename.empty())
return filename;
1047 return std::string();
1054 std::string_view basename{song.
filename};
1055 auto fnstart = basename.rfind(PATHSEPCHAR);
1056 if (fnstart != std::string_view::npos) basename.remove_prefix(fnstart + 1);
1059 tempdirname.reserve(tempdirname.size() + basename.size());
1060 for (
auto c : basename) {
1061 if (c !=
'.') tempdirname.append(1, c);
1068 std::string output_filename = fmt::format(
"{}{}.mid", tempdirname, song.
cat_index);
1072 return output_filename;
1076 if (!songdata.has_value())
return std::string();
1079 if (!midifile.
LoadMpsData(songdata->data(), songdata->size())) {
1080 return std::string();
1083 if (midifile.
WriteSMF(output_filename)) {
1084 return output_filename;
1086 return std::string();
1091static bool CmdDumpSMF(std::span<std::string_view> argv)
1094 IConsolePrint(
CC_HELP,
"Write the current song to a Standard MIDI File. Usage: 'dumpsmf <filename>'.");
1097 if (argv.size() != 2) {
1102 if (_midifile_instance ==
nullptr) {
1103 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.");
1110 if (_midifile_instance->WriteSMF(filename)) {
1119static void RegisterConsoleMidiCommands()
1121 static bool registered =
false;
1130 RegisterConsoleMidiCommands();
1136 if (_midifile_instance ==
this) {
1137 _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.
Console functions used outside of the console code.
Internally used functions for the console.
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.
Function to handling different endian machines.
#define T
Climate temperate.
void AppendPathSeparator(std::string &buf)
Appends, if necessary, the path separator character to the end of the string.
std::optional< FileHandle > FioFOpenFile(std::string_view filename, std::string_view mode, Subdirectory subdir, size_t *filesize)
Opens a OpenTTD file somewhere in a personal or global directory.
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(std::string_view filename)
Test whether the given filename exists.
std::string FioFindFullPath(Subdirectory subdir, std::string_view filename)
Find a path to the filename in one of the search directories.
Functions for standard in/out file operations.
std::string_view FiosGetScreenshotDir()
Get the directory for screenshots.
Types for standard in/out file operations.
@ 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).
Declarations for MIDI data.
MidiSysexMessage
Well-known MIDI system exclusive message values for use with the MidiGetStandardSysexMessage function...
@ ResetGM
Reset device to General MIDI defaults.
@ ResetGS
Reset device to (Roland) General Standard defaults.
@ ResetXG
Reset device to (Yamaha) XG defaults.
@ RolandSetReverb
Set up Roland SoundCanvas reverb room as TTD does.
@ MIDIST_SMF_META
only occurs in SMF data
@ MIDIST_SMF_ESCAPE
only occurs in SMF data
@ MIDIST_ENDSYSEX
only occurs in realtime data
const uint8_t * MidiGetStandardSysexMessage(MidiSysexMessage msg, size_t &length)
Retrieve a well-known MIDI system exclusive message.
Parser for standard MIDI files.
A number of safeguards to prevent using unsafe methods.
Definition of base types and functions in a cross-platform compatible way.
#define lengthof(array)
Return the length of an fixed size array.
Functions related to low-level strings.
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 ticktime
tick number since start of file this block should be triggered at
int64_t realtime
real-time (microseconds) 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
~MidiFile()
Remove ourselves from the _midifile_instance if needed.
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