12#include "midifile.hpp"
13#include "../fileio_func.h"
14#include "../fileio_type.h"
15#include "../string_func.h"
16#include "../core/endian_func.hpp"
17#include "../base_media_base.h"
18#include "../base_media_music.h"
21#include "../console_func.h"
22#include "../console_internal.h"
24#include "table/strings.h"
26#include "../safeguards.h"
31static MidiFile *_midifile_instance =
nullptr;
39const uint8_t *MidiGetStandardSysexMessage(MidiSysexMessage msg,
size_t &length)
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 };
47 case MidiSysexMessage::ResetGM:
49 return reset_gm_sysex;
50 case MidiSysexMessage::ResetGS:
52 return reset_gs_sysex;
53 case MidiSysexMessage::ResetXG:
55 return reset_xg_sysex;
56 case MidiSysexMessage::RolandSetReverb:
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]);
274 }
else if (status == MIDIST_SMF_META) {
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;
317 }
else if (status == MIDIST_SMF_ESCAPE) {
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);
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++) {
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++) {
629 uint16_t newdelay = 0;
635 b1 = this->songdata[chandata.
playpos++];
639 b1 = this->songdata[chandata.
playpos++];
662 this->shouldplayflag =
false;
671 b1 = this->songdata[chandata.
playpos++];
677 b2 = this->songdata[chandata.
playpos++];
683 velocity = (int16_t)b2 * 0x50;
688 b2 = (velocity / 128) & 0x00FF;
689 AddMidiData(outblock, MIDIST_NOTEON + channel, b1, b2);
692 AddMidiData(outblock, MIDIST_NOTEON + channel, b1, 0);
695 case MIDIST_CONTROLLER:
696 b2 = this->songdata[chandata.
playpos++];
697 if (b1 == MIDICT_MODE_MONO) {
703 }
else if (b1 == 0) {
707 this->current_tempo = ((int)b2) * 48 / 60;
710 }
else if (b1 == MIDICT_EFFECTS1) {
715 AddMidiData(outblock, MIDIST_CONTROLLER + channel, b1, b2);
723 this->shouldplayflag =
false;
730 if (b1 == 0x57 || b1 == 0x3F) {
733 AddMidiData(outblock, MIDIST_PROGCHG + channel, b1);
735 case MIDIST_PITCHBEND:
736 b2 = this->songdata[chandata.
playpos++];
737 AddMidiData(outblock, MIDIST_PITCHBEND + channel, b1, b2);
744 }
while (newdelay == 0);
756 if (this->tempo_ticks > 0) {
762 for (
int ch = 0; ch < 16; ch++) {
765 if (chandata.
delay == 0) {
789 this->shouldplayflag =
true;
790 this->current_tempo = (int32_t)this->initial_tempo * 24 / 60;
794 auto &data_block = this->target.
blocks.emplace_back();
795 AddMidiData(data_block, MIDIST_PROGCHG + 9, 0x00);
800 for (uint32_t tick = 0; tick < 100000; tick += 1) {
801 auto &block = this->target.
blocks.emplace_back();
814 100, 100, 100, 100, 100, 90, 100, 100, 100, 100, 100, 90, 100, 100, 100, 100,
815 100, 100, 85, 100, 100, 100, 100, 100, 100, 100, 100, 100, 90, 90, 110, 80,
816 100, 100, 100, 90, 70, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
817 100, 100, 90, 100, 100, 100, 100, 100, 100, 120, 100, 100, 100, 120, 100, 127,
818 100, 100, 90, 100, 100, 100, 100, 100, 100, 95, 100, 100, 100, 100, 100, 100,
819 100, 100, 100, 100, 100, 100, 100, 115, 100, 100, 100, 100, 100, 100, 100, 100,
820 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
821 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
832 _midifile_instance =
this;
835 return machine.
PlayInto() && FixupMidiData(*
this);
846 if (songdata.has_value()) {
847 bool result = this->
LoadMpsData(songdata->data(), songdata->size());
868 _midifile_instance =
this;
875static void WriteVariableLen(
FileHandle &f, uint32_t value)
879 fwrite(&tb, 1, 1, f);
880 }
else if (value <= 0x3FFF) {
882 tb[1] = value & 0x7F; value >>= 7;
883 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
884 fwrite(tb, 1,
sizeof(tb), f);
885 }
else if (value <= 0x1FFFFF) {
887 tb[2] = value & 0x7F; value >>= 7;
888 tb[1] = (value & 0x7F) | 0x80; value >>= 7;
889 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
890 fwrite(tb, 1,
sizeof(tb), f);
891 }
else if (value <= 0x0FFFFFFF) {
893 tb[3] = value & 0x7F; value >>= 7;
894 tb[2] = (value & 0x7F) | 0x80; value >>= 7;
895 tb[1] = (value & 0x7F) | 0x80; value >>= 7;
896 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
897 fwrite(tb, 1,
sizeof(tb), f);
909 if (!of.has_value())
return false;
913 const uint8_t fileheader[] = {
915 0x00, 0x00, 0x00, 0x06,
920 fwrite(fileheader,
sizeof(fileheader), 1, f);
923 const uint8_t trackheader[] = {
927 fwrite(trackheader,
sizeof(trackheader), 1, f);
929 size_t tracksizepos = ftell(f) - 4;
932 uint32_t lasttime = 0;
933 size_t nexttempoindex = 0;
934 for (
size_t bi = 0; bi < this->
blocks.size(); bi++) {
938 uint32_t timediff = block.
ticktime - lasttime;
942 timediff = nexttempo.
ticktime - lasttime;
946 lasttime += timediff;
947 bool needtime =
false;
948 WriteVariableLen(f, timediff);
952 uint8_t tempobuf[6] = { MIDIST_SMF_META, 0x51, 0x03, 0, 0, 0 };
953 tempobuf[3] = (nexttempo.
tempo & 0x00FF0000) >> 16;
954 tempobuf[4] = (nexttempo.
tempo & 0x0000FF00) >> 8;
955 tempobuf[5] = (nexttempo.
tempo & 0x000000FF);
956 fwrite(tempobuf,
sizeof(tempobuf), 1, f);
969 uint8_t *dp = block.
data.data();
970 while (dp < block.
data.data() + block.
data.size()) {
978 switch (*dp & 0xF0) {
981 case MIDIST_POLYPRESS:
982 case MIDIST_CONTROLLER:
983 case MIDIST_PITCHBEND:
988 case MIDIST_CHANPRESS:
995 if (*dp == MIDIST_SYSEX) {
998 uint8_t *sysexend = dp;
999 while (*sysexend != MIDIST_ENDSYSEX) sysexend++;
1000 ptrdiff_t sysexlen = sysexend - dp;
1001 WriteVariableLen(f, sysexlen);
1002 fwrite(dp, 1, sysexend - dp, f);
1013 static const uint8_t track_end_marker[] = { 0x00, MIDIST_SMF_META, 0x2F, 0x00 };
1014 fwrite(&track_end_marker,
sizeof(track_end_marker), 1, f);
1017 size_t trackendpos = ftell(f);
1018 fseek(f, tracksizepos, SEEK_SET);
1019 uint32_t tracksize = (uint32_t)(trackendpos - tracksizepos - 4);
1020 tracksize = TO_BE32(tracksize);
1021 fwrite(&tracksize, 4, 1, f);
1037 if (!filename.empty())
return filename;
1039 if (!filename.empty())
return filename;
1041 return std::string();
1048 std::string_view basename{song.
filename};
1049 auto fnstart = basename.rfind(PATHSEPCHAR);
1050 if (fnstart != std::string_view::npos) basename.remove_prefix(fnstart + 1);
1053 tempdirname.reserve(tempdirname.size() + basename.size());
1054 for (
auto c : basename) {
1055 if (c !=
'.') tempdirname.append(1, c);
1062 std::string output_filename = fmt::format(
"{}{}.mid", tempdirname, song.
cat_index);
1066 return output_filename;
1070 if (!songdata.has_value())
return std::string();
1073 if (!midifile.
LoadMpsData(songdata->data(), songdata->size())) {
1074 return std::string();
1077 if (midifile.
WriteSMF(output_filename)) {
1078 return output_filename;
1080 return std::string();
1085static bool CmdDumpSMF(std::span<std::string_view> argv)
1088 IConsolePrint(
CC_HELP,
"Write the current song to a Standard MIDI File. Usage: 'dumpsmf <filename>'.");
1091 if (argv.size() != 2) {
1096 if (_midifile_instance ==
nullptr) {
1097 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.");
1104 if (_midifile_instance->
WriteSMF(filename)) {
1113static void RegisterConsoleMidiCommands()
1115 static bool registered =
false;
1124 RegisterConsoleMidiCommands();
1127MidiFile::~MidiFile()
1129 if (_midifile_instance ==
this) {
1130 _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.
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.
std::string_view 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)
#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 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
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