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 "../base_media_base.h"
16#include "../base_media_music.h"
19#include "../console_func.h"
20#include "../console_internal.h"
22#include "table/strings.h"
27static MidiFile *_midifile_instance =
nullptr;
35const uint8_t *MidiGetStandardSysexMessage(MidiSysexMessage msg,
size_t &length)
37 static uint8_t reset_gm_sysex[] = { 0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7 };
38 static uint8_t reset_gs_sysex[] = { 0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7 };
39 static uint8_t reset_xg_sysex[] = { 0xF0, 0x43, 0x10, 0x4C, 0x00, 0x00, 0x7E, 0x00, 0xF7 };
40 static uint8_t roland_reverb_sysex[] = { 0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x01, 0x30, 0x02, 0x04, 0x00, 0x40, 0x40, 0x00, 0x00, 0x09, 0xF7 };
43 case MidiSysexMessage::ResetGM:
45 return reset_gm_sysex;
46 case MidiSysexMessage::ResetGS:
48 return reset_gs_sysex;
49 case MidiSysexMessage::ResetXG:
51 return reset_xg_sysex;
52 case MidiSysexMessage::RolandSetReverb:
53 length =
lengthof(roland_reverb_sysex);
54 return roland_reverb_sysex;
65 std::vector<uint8_t> buf{};
77 this->buf.resize(len);
78 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_length > 1024 * 1024)
return false;
212 if (!chunk.IsValid()) {
218 uint8_t last_status = 0;
219 bool running_sysex =
false;
220 while (!chunk.IsEnd()) {
222 uint32_t deltatime = 0;
223 if (!chunk.ReadVariableLength(deltatime)) {
232 if (!chunk.ReadByte(status)) {
236 if ((status & 0x80) == 0) {
240 status = last_status;
242 }
else if ((status & 0xF0) != 0xF0) {
244 last_status = status;
246 switch (status & 0xF0) {
249 case MIDIST_POLYPRESS:
250 case MIDIST_CONTROLLER:
251 case MIDIST_PITCHBEND:
253 block->
data.push_back(status);
254 if (!chunk.ReadDataBlock(block, 2)) {
259 case MIDIST_CHANPRESS:
261 block->
data.push_back(status);
262 if (!chunk.ReadByte(buf[0])) {
265 block->
data.push_back(buf[0]);
270 }
else if (status == MIDIST_SMF_META) {
272 if (!chunk.ReadByte(buf[0])) {
276 if (!chunk.ReadVariableLength(length)) {
282 return (length == 0);
285 if (length != 3)
return false;
286 if (!chunk.ReadBuffer(buf, 3))
return false;
291 if (!chunk.Skip(length)) {
296 }
else if (status == MIDIST_SYSEX || (status == MIDIST_SMF_ESCAPE && running_sysex)) {
299 if (!chunk.ReadVariableLength(length)) {
302 block->
data.push_back(0xF0);
303 if (!chunk.ReadDataBlock(block, length)) {
306 if (block->
data.back() != 0xF7) {
308 running_sysex =
true;
309 block->
data.push_back(0xF7);
311 running_sysex =
false;
313 }
else if (status == MIDIST_SMF_ESCAPE) {
316 if (!chunk.ReadVariableLength(length)) {
319 if (!chunk.ReadDataBlock(block, length)) {
340bool TicktimeAscending(
const T &a,
const T &b)
342 return a.ticktime < b.ticktime;
345static bool FixupMidiData(
MidiFile &target)
348 std::sort(target.
tempos.begin(), target.
tempos.end(), TicktimeAscending<MidiFile::TempoChange>);
349 std::sort(target.
blocks.begin(), target.
blocks.end(), TicktimeAscending<MidiFile::DataBlock>);
351 if (target.
tempos.empty()) {
359 std::vector<MidiFile::DataBlock> merged_blocks;
360 uint32_t last_ticktime = 0;
361 for (
size_t i = 0; i < target.
blocks.size(); i++) {
363 if (block.
data.empty()) {
365 }
else if (block.
ticktime > last_ticktime || merged_blocks.empty()) {
366 merged_blocks.push_back(block);
369 merged_blocks.back().data.insert(merged_blocks.back().data.end(), block.
data.begin(), block.
data.end());
372 std::swap(merged_blocks, target.
blocks);
376 uint32_t last_realtime = 0;
377 size_t cur_tempo = 0, cur_block = 0;
378 while (cur_block < target.
blocks.size()) {
384 int64_t tickdiff = block.
ticktime - last_ticktime;
386 last_realtime += uint32_t(tickdiff * tempo.
tempo / target.
tickdiv);
391 int64_t tickdiff = next_tempo.
ticktime - last_ticktime;
392 last_ticktime = next_tempo.
ticktime;
393 last_realtime += uint32_t(tickdiff * tempo.
tempo / target.
tickdiv);
410 if (!file.has_value())
return false;
426 if (fread(buffer,
sizeof(buffer), 1, file) != 1) {
431 const uint8_t magic[] = {
'M',
'T',
'h',
'd', 0x00, 0x00, 0x00, 0x06 };
432 if (!std::ranges::equal(std::span(buffer, std::size(magic)), magic)) {
437 header.format = (buffer[8] << 8) | buffer[9];
438 header.tracks = (buffer[10] << 8) | buffer[11];
439 header.tickdiv = (buffer[12] << 8) | buffer[13];
450 _midifile_instance =
this;
457 if (!file.has_value())
return false;
463 if (header.format != 0 && header.format != 1)
return false;
465 if ((header.tickdiv & 0x8000) != 0)
return false;
467 if (header.tickdiv == 0)
return false;
469 this->
tickdiv = header.tickdiv;
471 for (; header.tracks > 0; header.tracks--) {
472 if (!ReadTrackChunk(*file, *
this)) {
477 return FixupMidiData(*
this);
535 block.
data.push_back(b1);
536 block.
data.push_back(b2);
540 block.
data.push_back(b1);
541 block.
data.push_back(b2);
542 block.
data.push_back(b3);
559 this->initial_tempo = this->songdata[pos++];
562 loopmax = this->songdata[pos++];
563 for (loopidx = 0; loopidx < loopmax; loopidx++) {
569 pos += FROM_LE16(*(
const int16_t *)(this->songdata + pos));
574 loopmax = this->songdata[pos++];
575 for (loopidx = 0; loopidx < loopmax; loopidx++) {
579 uint8_t ch = this->songdata[pos++];
580 this->
channels[ch].startpos = pos + 4;
581 pos += FROM_LE16(*(
const int16_t *)(this->songdata + pos));
595 b = this->songdata[pos++];
596 res = (res << 7) + (b & 0x7F);
606 for (
int ch = 0; ch < 16; ch++) {
625 uint16_t newdelay = 0;
631 b1 = this->songdata[chandata.
playpos++];
635 b1 = this->songdata[chandata.
playpos++];
658 this->shouldplayflag =
false;
667 b1 = this->songdata[chandata.
playpos++];
673 b2 = this->songdata[chandata.
playpos++];
679 velocity = (int16_t)b2 * 0x50;
684 b2 = (velocity / 128) & 0x00FF;
685 AddMidiData(outblock, MIDIST_NOTEON + channel, b1, b2);
688 AddMidiData(outblock, MIDIST_NOTEON + channel, b1, 0);
691 case MIDIST_CONTROLLER:
692 b2 = this->songdata[chandata.
playpos++];
693 if (b1 == MIDICT_MODE_MONO) {
699 }
else if (b1 == 0) {
703 this->current_tempo = ((int)b2) * 48 / 60;
706 }
else if (b1 == MIDICT_EFFECTS1) {
711 AddMidiData(outblock, MIDIST_CONTROLLER + channel, b1, b2);
719 this->shouldplayflag =
false;
726 if (b1 == 0x57 || b1 == 0x3F) {
729 AddMidiData(outblock, MIDIST_PROGCHG + channel, b1);
731 case MIDIST_PITCHBEND:
732 b2 = this->songdata[chandata.
playpos++];
733 AddMidiData(outblock, MIDIST_PITCHBEND + channel, b1, b2);
740 }
while (newdelay == 0);
752 if (this->tempo_ticks > 0) {
758 for (
int ch = 0; ch < 16; ch++) {
761 if (chandata.
delay == 0) {
785 this->shouldplayflag =
true;
786 this->current_tempo = (int32_t)this->initial_tempo * 24 / 60;
790 auto &data_block = this->target.
blocks.emplace_back();
791 AddMidiData(data_block, MIDIST_PROGCHG + 9, 0x00);
796 for (uint32_t tick = 0; tick < 100000; tick += 1) {
797 auto &block = this->target.
blocks.emplace_back();
810 100, 100, 100, 100, 100, 90, 100, 100, 100, 100, 100, 90, 100, 100, 100, 100,
811 100, 100, 85, 100, 100, 100, 100, 100, 100, 100, 100, 100, 90, 90, 110, 80,
812 100, 100, 100, 90, 70, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
813 100, 100, 90, 100, 100, 100, 100, 100, 100, 120, 100, 100, 100, 120, 100, 127,
814 100, 100, 90, 100, 100, 100, 100, 100, 100, 95, 100, 100, 100, 100, 100, 100,
815 100, 100, 100, 100, 100, 100, 100, 115, 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,
817 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
828 _midifile_instance =
this;
831 return machine.
PlayInto() && FixupMidiData(*
this);
842 if (songdata.has_value()) {
843 bool result = this->
LoadMpsData(songdata->data(), songdata->size());
864 _midifile_instance =
this;
871static void WriteVariableLen(
FileHandle &f, uint32_t value)
875 fwrite(&tb, 1, 1, f);
876 }
else if (value <= 0x3FFF) {
878 tb[1] = value & 0x7F; value >>= 7;
879 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
880 fwrite(tb, 1,
sizeof(tb), f);
881 }
else if (value <= 0x1FFFFF) {
883 tb[2] = value & 0x7F; 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);
887 }
else if (value <= 0x0FFFFFFF) {
889 tb[3] = value & 0x7F; value >>= 7;
890 tb[2] = (value & 0x7F) | 0x80; value >>= 7;
891 tb[1] = (value & 0x7F) | 0x80; value >>= 7;
892 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
893 fwrite(tb, 1,
sizeof(tb), f);
905 if (!of.has_value())
return false;
909 const uint8_t fileheader[] = {
911 0x00, 0x00, 0x00, 0x06,
916 fwrite(fileheader,
sizeof(fileheader), 1, f);
919 const uint8_t trackheader[] = {
923 fwrite(trackheader,
sizeof(trackheader), 1, f);
925 size_t tracksizepos = ftell(f) - 4;
928 uint32_t lasttime = 0;
929 size_t nexttempoindex = 0;
930 for (
size_t bi = 0; bi < this->
blocks.size(); bi++) {
934 uint32_t timediff = block.
ticktime - lasttime;
938 timediff = nexttempo.
ticktime - lasttime;
942 lasttime += timediff;
943 bool needtime =
false;
944 WriteVariableLen(f, timediff);
948 uint8_t tempobuf[6] = { MIDIST_SMF_META, 0x51, 0x03, 0, 0, 0 };
949 tempobuf[3] = (nexttempo.
tempo & 0x00FF0000) >> 16;
950 tempobuf[4] = (nexttempo.
tempo & 0x0000FF00) >> 8;
951 tempobuf[5] = (nexttempo.
tempo & 0x000000FF);
952 fwrite(tempobuf,
sizeof(tempobuf), 1, f);
965 uint8_t *dp = block.
data.data();
966 while (dp < block.
data.data() + block.
data.size()) {
974 switch (*dp & 0xF0) {
977 case MIDIST_POLYPRESS:
978 case MIDIST_CONTROLLER:
979 case MIDIST_PITCHBEND:
984 case MIDIST_CHANPRESS:
991 if (*dp == MIDIST_SYSEX) {
994 uint8_t *sysexend = dp;
995 while (*sysexend != MIDIST_ENDSYSEX) sysexend++;
996 ptrdiff_t sysexlen = sysexend - dp;
997 WriteVariableLen(f, sysexlen);
998 fwrite(dp, 1, sysexend - dp, f);
1009 static const uint8_t track_end_marker[] = { 0x00, MIDIST_SMF_META, 0x2F, 0x00 };
1010 fwrite(&track_end_marker,
sizeof(track_end_marker), 1, f);
1013 size_t trackendpos = ftell(f);
1014 fseek(f, tracksizepos, SEEK_SET);
1015 uint32_t tracksize = (uint32_t)(trackendpos - tracksizepos - 4);
1016 tracksize = TO_BE32(tracksize);
1017 fwrite(&tracksize, 4, 1, f);
1033 if (!filename.empty())
return filename;
1035 if (!filename.empty())
return filename;
1037 return std::string();
1042 char basename[MAX_PATH];
1044 const char *fnstart = strrchr(song.
filename.c_str(), PATHSEPCHAR);
1045 if (fnstart ==
nullptr) {
1052 char *wp = basename;
1053 for (
const char *rp = fnstart; *rp !=
'\0'; rp++) {
1054 if (*rp !=
'.') *wp++ = *rp;
1060 tempdirname += basename;
1064 std::string output_filename = tempdirname + std::to_string(song.
cat_index) +
".mid";
1068 return output_filename;
1072 if (!songdata.has_value())
return std::string();
1075 if (!midifile.
LoadMpsData(songdata->data(), songdata->size())) {
1076 return std::string();
1079 if (midifile.
WriteSMF(output_filename)) {
1080 return output_filename;
1082 return std::string();
1087static bool CmdDumpSMF(uint8_t argc,
char *argv[])
1090 IConsolePrint(
CC_HELP,
"Write the current song to a Standard MIDI File. Usage: 'dumpsmf <filename>'.");
1098 if (_midifile_instance ==
nullptr) {
1099 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.");
1106 if (_midifile_instance->
WriteSMF(filename)) {
1115static void RegisterConsoleMidiCommands()
1117 static bool registered =
false;
1126 RegisterConsoleMidiCommands();
1129MidiFile::~MidiFile()
1131 if (_midifile_instance ==
this) {
1132 _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)
#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