OpenTTD Source 20260311-master-g511d3794ce
midifile.cpp
Go to the documentation of this file.
1/*
2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <https://www.gnu.org/licenses/old-licenses/gpl-2.0>.
6 */
7
9
10#include "../stdafx.h"
11
12#include "midifile.hpp"
13#include "../fileio_func.h"
14#include "../fileio_type.h"
15#include "../string_func.h"
17#include "../base_media_base.h"
18#include "../base_media_music.h"
19#include "midi.h"
20
21#include "../console_func.h"
22#include "../console_internal.h"
23
24#include "table/strings.h"
25
26#include "../safeguards.h"
27
28/* SMF reader based on description at: http://www.somascape.org/midi/tech/mfile.html */
29
30
31static MidiFile *_midifile_instance = nullptr;
32
39const uint8_t *MidiGetStandardSysexMessage(MidiSysexMessage msg, size_t &length)
40{
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 };
45
46 switch (msg) {
48 length = lengthof(reset_gm_sysex);
49 return reset_gm_sysex;
51 length = lengthof(reset_gs_sysex);
52 return reset_gs_sysex;
54 length = lengthof(reset_xg_sysex);
55 return reset_xg_sysex;
57 length = lengthof(roland_reverb_sysex);
58 return roland_reverb_sysex;
59 default:
60 NOT_REACHED();
61 }
62}
63
69 std::vector<uint8_t> buf{};
70 size_t pos = 0;
71public:
79 ByteBuffer(FileHandle &file, size_t len)
80 {
81 this->buf.resize(len);
82 if (fread(this->buf.data(), 1, len, file) != len) {
83 /* invalid state */
84 this->buf.clear();
85 }
86 }
87
92 bool IsValid() const
93 {
94 return !this->buf.empty();
95 }
96
101 bool IsEnd() const
102 {
103 return this->pos >= this->buf.size();
104 }
105
111 bool ReadByte(uint8_t &b)
112 {
113 if (this->IsEnd()) return false;
114 b = this->buf[this->pos++];
115 return true;
116 }
117
125 bool ReadVariableLength(uint32_t &res)
126 {
127 res = 0;
128 uint8_t b = 0;
129 do {
130 if (this->IsEnd()) return false;
131 b = this->buf[this->pos++];
132 res = (res << 7) | (b & 0x7F);
133 } while (b & 0x80);
134 return true;
135 }
136
143 bool ReadBuffer(uint8_t *dest, size_t length)
144 {
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);
148 this->pos += length;
149 return true;
150 }
151
158 bool ReadDataBlock(MidiFile::DataBlock *dest, size_t length)
159 {
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);
163 this->pos += length;
164 return true;
165 }
166
172 bool Skip(size_t count)
173 {
174 if (this->IsEnd()) return false;
175 if (this->buf.size() - this->pos < count) return false;
176 this->pos += count;
177 return true;
178 }
179
185 bool Rewind(size_t count)
186 {
187 if (count > this->pos) return false;
188 this->pos -= count;
189 return true;
190 }
191};
192
193static bool ReadTrackChunk(FileHandle &file, MidiFile &target)
194{
195 uint8_t buf[4];
196
197 const uint8_t magic[] = { 'M', 'T', 'r', 'k' };
198 if (fread(buf, sizeof(magic), 1, file) != 1) {
199 return false;
200 }
201 if (!std::ranges::equal(magic, buf)) {
202 return false;
203 }
204
205 /* Read chunk length and then the whole chunk */
206 uint32_t chunk_length;
207 if (fread(&chunk_length, 1, 4, file) != 4) {
208 return false;
209 }
210 chunk_length = FROM_BE32(chunk_length);
211
212 /* Limit chunk size to 1 MiB. */
213 if (chunk_length > 1024 * 1024) return false;
214
215 ByteBuffer chunk(file, chunk_length);
216 if (!chunk.IsValid()) {
217 return false;
218 }
219
220 MidiFile::DataBlock *block = &target.blocks.emplace_back();
221
222 uint8_t last_status = 0;
223 bool running_sysex = false;
224 while (!chunk.IsEnd()) {
225 /* Read deltatime for event, start new block */
226 uint32_t deltatime = 0;
227 if (!chunk.ReadVariableLength(deltatime)) {
228 return false;
229 }
230 if (deltatime > 0) {
231 block = &target.blocks.emplace_back(block->ticktime + deltatime);
232 }
233
234 /* Read status byte */
235 uint8_t status;
236 if (!chunk.ReadByte(status)) {
237 return false;
238 }
239
240 if ((status & 0x80) == 0) {
241 /* High bit not set means running status message, status is same as last
242 * convert to explicit status */
243 chunk.Rewind(1);
244 status = last_status;
245 goto running_status;
246 } else if ((status & 0xF0) != 0xF0) {
247 /* Regular channel message */
248 last_status = status;
249 running_status:
250 switch (status & 0xF0) {
251 case MIDIST_NOTEOFF:
252 case MIDIST_NOTEON:
253 case MIDIST_POLYPRESS:
254 case MIDIST_CONTROLLER:
255 case MIDIST_PITCHBEND:
256 /* 3 byte messages */
257 block->data.push_back(status);
258 if (!chunk.ReadDataBlock(block, 2)) {
259 return false;
260 }
261 break;
262 case MIDIST_PROGCHG:
263 case MIDIST_CHANPRESS:
264 /* 2 byte messages */
265 block->data.push_back(status);
266 if (!chunk.ReadByte(buf[0])) {
267 return false;
268 }
269 block->data.push_back(buf[0]);
270 break;
271 default:
272 NOT_REACHED();
273 }
274 } else if (status == MIDIST_SMF_META) {
275 /* Meta event, read event type byte and data length */
276 if (!chunk.ReadByte(buf[0])) {
277 return false;
278 }
279 uint32_t length = 0;
280 if (!chunk.ReadVariableLength(length)) {
281 return false;
282 }
283 switch (buf[0]) {
284 case 0x2F:
285 /* end of track, no more data (length != 0 is illegal) */
286 return (length == 0);
287 case 0x51:
288 /* tempo change */
289 if (length != 3) return false;
290 if (!chunk.ReadBuffer(buf, 3)) return false;
291 target.tempos.push_back(MidiFile::TempoChange(block->ticktime, buf[0] << 16 | buf[1] << 8 | buf[2]));
292 break;
293 default:
294 /* unimportant meta event, skip over it */
295 if (!chunk.Skip(length)) {
296 return false;
297 }
298 break;
299 }
300 } else if (status == MIDIST_SYSEX || (status == MIDIST_SMF_ESCAPE && running_sysex)) {
301 /* System exclusive message */
302 uint32_t length = 0;
303 if (!chunk.ReadVariableLength(length)) {
304 return false;
305 }
306 block->data.push_back(0xF0);
307 if (!chunk.ReadDataBlock(block, length)) {
308 return false;
309 }
310 if (block->data.back() != 0xF7) {
311 /* Engage Casio weirdo mode - convert to normal sysex */
312 running_sysex = true;
313 block->data.push_back(0xF7);
314 } else {
315 running_sysex = false;
316 }
317 } else if (status == MIDIST_SMF_ESCAPE) {
318 /* Escape sequence */
319 uint32_t length = 0;
320 if (!chunk.ReadVariableLength(length)) {
321 return false;
322 }
323 if (!chunk.ReadDataBlock(block, length)) {
324 return false;
325 }
326 } else {
327 /* Messages undefined in standard midi files:
328 * 0xF1 - MIDI time code quarter frame
329 * 0xF2 - Song position pointer
330 * 0xF3 - Song select
331 * 0xF4 - undefined/reserved
332 * 0xF5 - undefined/reserved
333 * 0xF6 - Tune request for analog synths
334 * 0xF8..0xFE - System real-time messages
335 */
336 return false;
337 }
338 }
339
340 NOT_REACHED();
341}
342
343template <typename T>
344bool TicktimeAscending(const T &a, const T &b)
345{
346 return a.ticktime < b.ticktime;
347}
348
349static bool FixupMidiData(MidiFile &target)
350{
351 /* Sort all tempo changes and events */
352 std::sort(target.tempos.begin(), target.tempos.end(), TicktimeAscending<MidiFile::TempoChange>);
353 std::sort(target.blocks.begin(), target.blocks.end(), TicktimeAscending<MidiFile::DataBlock>);
354
355 if (target.tempos.empty()) {
356 /* No tempo information, assume 120 bpm (500,000 microseconds per beat */
357 target.tempos.push_back(MidiFile::TempoChange(0, 500000));
358 }
359 /* Add sentinel tempo at end */
360 target.tempos.push_back(MidiFile::TempoChange(UINT32_MAX, 0));
361
362 /* Merge blocks with identical tick times */
363 std::vector<MidiFile::DataBlock> merged_blocks;
364 uint32_t last_ticktime = 0;
365 for (size_t i = 0; i < target.blocks.size(); i++) {
366 MidiFile::DataBlock &block = target.blocks[i];
367 if (block.data.empty()) {
368 continue;
369 } else if (block.ticktime > last_ticktime || merged_blocks.empty()) {
370 merged_blocks.push_back(block);
371 last_ticktime = block.ticktime;
372 } else {
373 merged_blocks.back().data.insert(merged_blocks.back().data.end(), block.data.begin(), block.data.end());
374 }
375 }
376 std::swap(merged_blocks, target.blocks);
377
378 /* Annotate blocks with real time */
379 last_ticktime = 0;
380 int64_t last_realtime = 0;
381 size_t cur_tempo = 0, cur_block = 0;
382 while (cur_block < target.blocks.size()) {
383 MidiFile::DataBlock &block = target.blocks[cur_block];
384 MidiFile::TempoChange &tempo = target.tempos[cur_tempo];
385 MidiFile::TempoChange &next_tempo = target.tempos[cur_tempo + 1];
386 if (block.ticktime <= next_tempo.ticktime) {
387 /* block is within the current tempo */
388 int64_t tickdiff = block.ticktime - last_ticktime;
389 last_ticktime = block.ticktime;
390 last_realtime += tickdiff * tempo.tempo / target.tickdiv;
391 block.realtime = last_realtime;
392 cur_block++;
393 } else {
394 /* tempo change occurs before this block */
395 int64_t tickdiff = next_tempo.ticktime - last_ticktime;
396 last_ticktime = next_tempo.ticktime;
397 last_realtime += tickdiff * tempo.tempo / target.tickdiv; // current tempo until the tempo change
398 cur_tempo++;
399 }
400 }
401
402 return true;
403}
404
411bool MidiFile::ReadSMFHeader(const std::string &filename, SMFHeader &header)
412{
413 auto file = FioFOpenFile(filename, "rb", Subdirectory::BASESET_DIR);
414 if (!file.has_value()) return false;
415 bool result = ReadSMFHeader(*file, header);
416 return result;
417}
418
427{
428 /* Try to read header, fixed size */
429 uint8_t buffer[14];
430 if (fread(buffer, sizeof(buffer), 1, file) != 1) {
431 return false;
432 }
433
434 /* Check magic, 'MThd' followed by 4 byte length indicator (always = 6 in SMF) */
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)) {
437 return false;
438 }
439
440 /* Read the parameters of the file */
441 header.format = (buffer[8] << 8) | buffer[9];
442 header.tracks = (buffer[10] << 8) | buffer[11];
443 header.tickdiv = (buffer[12] << 8) | buffer[13];
444 return true;
445}
446
452bool MidiFile::LoadFile(const std::string &filename)
453{
454 _midifile_instance = this;
455
456 this->blocks.clear();
457 this->tempos.clear();
458 this->tickdiv = 0;
459
460 auto file = FioFOpenFile(filename, "rb", Subdirectory::BASESET_DIR);
461 if (!file.has_value()) return false;
462
463 SMFHeader header;
464 if (!ReadSMFHeader(*file, header)) return false;
465
466 /* Only format 0 (single-track) and format 1 (multi-track single-song) are accepted for now */
467 if (header.format != 0 && header.format != 1) return false;
468 /* Doesn't support SMPTE timecode files */
469 if ((header.tickdiv & 0x8000) != 0) return false;
470 /* Ticks per beat / parts per quarter note should not be zero. */
471 if (header.tickdiv == 0) return false;
472
473 this->tickdiv = header.tickdiv;
474
475 for (; header.tracks > 0; header.tracks--) {
476 if (!ReadTrackChunk(*file, *this)) {
477 return false;
478 }
479 }
480
481 return FixupMidiData(*this);
482}
483
484
508 struct Channel {
509 uint8_t cur_program = 0xFF;
510 uint8_t running_status = 0;
511 uint16_t delay = 0;
512 uint32_t playpos = 0;
513 uint32_t startpos = 0;
514 uint32_t returnpos = 0;
515 };
516 std::array<Channel, 16> channels{};
517 std::vector<uint32_t> segments{};
518 int16_t tempo_ticks = 0;
519 int16_t current_tempo = 0;
520 int16_t initial_tempo = 0;
521 bool shouldplayflag = false;
522
523 static const int TEMPO_RATE;
524 static const uint8_t programvelocities[128];
525
526 const uint8_t *songdata = nullptr;
527 size_t songdatalen = 0;
529
536
537 static void AddMidiData(MidiFile::DataBlock &block, uint8_t b1, uint8_t b2)
538 {
539 block.data.push_back(b1);
540 block.data.push_back(b2);
541 }
542 static void AddMidiData(MidiFile::DataBlock &block, uint8_t b1, uint8_t b2, uint8_t b3)
543 {
544 block.data.push_back(b1);
545 block.data.push_back(b2);
546 block.data.push_back(b3);
547 }
548
555 MpsMachine(const uint8_t *data, size_t length, MidiFile &target)
556 : songdata(data), songdatalen(length), target(target)
557 {
558 uint32_t pos = 0;
559 int loopmax;
560 int loopidx;
561
562 /* First byte is the initial "tempo" */
563 this->initial_tempo = this->songdata[pos++];
564
565 /* Next byte is a count of callable segments */
566 loopmax = this->songdata[pos++];
567 for (loopidx = 0; loopidx < loopmax; loopidx++) {
568 /* Segments form a linked list in the stream,
569 * first two bytes in each is an offset to the next.
570 * Two bytes between offset to next and start of data
571 * are unaccounted for. */
572 this->segments.push_back(pos + 4);
573 pos += FROM_LE16(*(const int16_t *)(this->songdata + pos));
574 }
575
576 /* After segments follows list of master tracks for each channel,
577 * also prefixed with a byte counting actual tracks. */
578 loopmax = this->songdata[pos++];
579 for (loopidx = 0; loopidx < loopmax; loopidx++) {
580 /* Similar structure to segments list, but also has
581 * the MIDI channel number as a byte before the offset
582 * to next track. */
583 uint8_t ch = this->songdata[pos++];
584 this->channels[ch].startpos = pos + 4;
585 pos += FROM_LE16(*(const int16_t *)(this->songdata + pos));
586 }
587 }
588
594 uint16_t ReadVariableLength(uint32_t &pos)
595 {
596 uint8_t b = 0;
597 uint16_t res = 0;
598 do {
599 b = this->songdata[pos++];
600 res = (res << 7) + (b & 0x7F);
601 } while (b & 0x80);
602 return res;
603 }
604
609 {
610 for (int ch = 0; ch < 16; ch++) {
611 Channel &chandata = this->channels[ch];
612 if (chandata.startpos != 0) {
613 /* Active track, set position to beginning */
614 chandata.playpos = chandata.startpos;
615 chandata.delay = this->ReadVariableLength(chandata.playpos);
616 } else {
617 /* Inactive track, mark as such */
618 chandata.playpos = 0;
619 chandata.delay = 0;
620 }
621 }
622 }
623
630 uint16_t PlayChannelFrame(MidiFile::DataBlock &outblock, int channel)
631 {
632 uint16_t newdelay = 0;
633 uint8_t b1, b2;
634 Channel &chandata = this->channels[channel];
635
636 do {
637 /* Read command/status byte */
638 b1 = this->songdata[chandata.playpos++];
639
640 /* Command 0xFE, call segment from master track */
641 if (b1 == MPSMIDIST_SEGMENT_CALL) {
642 b1 = this->songdata[chandata.playpos++];
643 chandata.returnpos = chandata.playpos;
644 chandata.playpos = this->segments[b1];
645 newdelay = this->ReadVariableLength(chandata.playpos);
646 if (newdelay == 0) {
647 continue;
648 }
649 return newdelay;
650 }
651
652 /* Command 0xFD, return from segment to master track */
653 if (b1 == MPSMIDIST_SEGMENT_RETURN) {
654 chandata.playpos = chandata.returnpos;
655 chandata.returnpos = 0;
656 newdelay = this->ReadVariableLength(chandata.playpos);
657 if (newdelay == 0) {
658 continue;
659 }
660 return newdelay;
661 }
662
663 /* Command 0xFF, end of song */
664 if (b1 == MPSMIDIST_ENDSONG) {
665 this->shouldplayflag = false;
666 return 0;
667 }
668
669 /* Regular MIDI channel message status byte */
670 if (b1 >= 0x80) {
671 /* Save the status byte as running status for the channel
672 * and read another byte for first parameter to command */
673 chandata.running_status = b1;
674 b1 = this->songdata[chandata.playpos++];
675 }
676
677 switch (chandata.running_status & 0xF0) {
678 case MIDIST_NOTEOFF:
679 case MIDIST_NOTEON:
680 b2 = this->songdata[chandata.playpos++];
681 if (b2 != 0) {
682 /* Note on, read velocity and scale according to rules */
683 int16_t velocity;
684 if (channel == 9) {
685 /* Percussion channel, fixed velocity scaling not in the table */
686 velocity = (int16_t)b2 * 0x50;
687 } else {
688 /* Regular channel, use scaling from table */
689 velocity = b2 * programvelocities[chandata.cur_program];
690 }
691 b2 = (velocity / 128) & 0x00FF;
692 AddMidiData(outblock, MIDIST_NOTEON + channel, b1, b2);
693 } else {
694 /* Note off */
695 AddMidiData(outblock, MIDIST_NOTEON + channel, b1, 0);
696 }
697 break;
698 case MIDIST_CONTROLLER:
699 b2 = this->songdata[chandata.playpos++];
700 if (b1 == MIDICT_MODE_MONO) {
701 /* Unknown what the purpose of this is.
702 * Occurs in "Can't get There from Here" and in "Aliens Ate my Railway" a few times each.
703 * Possibly intended to give hints to other (non-GM) music drivers decoding the song.
704 */
705 break;
706 } else if (b1 == 0) {
707 /* Standard MIDI controller 0 is "bank select", override meaning to change tempo.
708 * This is not actually used in any of the original songs. */
709 if (b2 != 0) {
710 this->current_tempo = ((int)b2) * 48 / 60;
711 }
712 break;
713 } else if (b1 == MIDICT_EFFECTS1) {
714 /* Override value of this controller, default mapping is Reverb Send Level according to MMA RP-023.
715 * Unknown what the purpose of this particular value is. */
716 b2 = 30;
717 }
718 AddMidiData(outblock, MIDIST_CONTROLLER + channel, b1, b2);
719 break;
720 case MIDIST_PROGCHG:
721 if (b1 == 0x7E) {
722 /* Program change to "Applause" is originally used
723 * to cause the song to loop, but that gets handled
724 * separately in the output driver here.
725 * Just end the song. */
726 this->shouldplayflag = false;
727 break;
728 }
729 /* Used for note velocity scaling lookup */
730 chandata.cur_program = b1;
731 /* Two programs translated to a third, this is likely to
732 * provide three different velocity scalings of "brass". */
733 if (b1 == 0x57 || b1 == 0x3F) {
734 b1 = 0x3E;
735 }
736 AddMidiData(outblock, MIDIST_PROGCHG + channel, b1);
737 break;
738 case MIDIST_PITCHBEND:
739 b2 = this->songdata[chandata.playpos++];
740 AddMidiData(outblock, MIDIST_PITCHBEND + channel, b1, b2);
741 break;
742 default:
743 break;
744 }
745
746 newdelay = this->ReadVariableLength(chandata.playpos);
747 } while (newdelay == 0);
748
749 return newdelay;
750 }
751
758 {
759 /* Update tempo/ticks counter */
760 this->tempo_ticks -= this->current_tempo;
761 if (this->tempo_ticks > 0) {
762 return true;
763 }
764 this->tempo_ticks += TEMPO_RATE;
765
766 /* Look over all channels, play those active */
767 for (int ch = 0; ch < 16; ch++) {
768 Channel &chandata = this->channels[ch];
769 if (chandata.playpos != 0) {
770 if (chandata.delay == 0) {
771 chandata.delay = this->PlayChannelFrame(block, ch);
772 }
773 chandata.delay--;
774 }
775 }
776
777 return this->shouldplayflag;
778 }
779
784 bool PlayInto()
785 {
786 /* Tempo seems to be handled as TEMPO_RATE = 148 ticks per second.
787 * Use this as the tickdiv, and define the tempo to be somewhat less than one second (1M microseconds) per quarter note.
788 * This value was found experimentally to give a very close approximation of the correct playback speed.
789 * MIDI software loading exported files will show a bogus tempo, but playback will be correct. */
790 this->target.tickdiv = TEMPO_RATE;
791 this->target.tempos.push_back(MidiFile::TempoChange(0, 980500));
792
793 /* Initialize playback simulation */
794 this->RestartSong();
795 this->shouldplayflag = true;
796 this->current_tempo = (int32_t)this->initial_tempo * 24 / 60;
797 this->tempo_ticks = this->current_tempo;
798
799 /* Always reset percussion channel to program 0 */
800 auto &data_block = this->target.blocks.emplace_back();
801 AddMidiData(data_block, MIDIST_PROGCHG + 9, 0x00);
802
803 /* Technically should be an endless loop, but having
804 * a maximum (about 10 minutes) avoids getting stuck,
805 * in case of corrupted data. */
806 for (uint32_t tick = 0; tick < 100000; tick += 1) {
807 auto &block = this->target.blocks.emplace_back();
808 block.ticktime = tick;
809 if (!this->PlayFrame(block)) {
810 break;
811 }
812 }
813 return true;
814 }
815};
816
817const int MpsMachine::TEMPO_RATE = 148;
819const uint8_t MpsMachine::programvelocities[128] = {
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,
828};
829
836bool MidiFile::LoadMpsData(const uint8_t *data, size_t length)
837{
838 _midifile_instance = this;
839
840 MpsMachine machine(data, length, *this);
841 return machine.PlayInto() && FixupMidiData(*this);
842}
843
844bool MidiFile::LoadSong(const MusicSongInfo &song)
845{
846 switch (song.filetype) {
847 case MTT_STANDARDMIDI:
848 return this->LoadFile(song.filename);
849 case MTT_MPSMIDI:
850 {
851 auto songdata = GetMusicCatEntryData(song.filename, song.cat_index);
852 if (songdata.has_value()) {
853 bool result = this->LoadMpsData(songdata->data(), songdata->size());
854 return result;
855 } else {
856 return false;
857 }
858 }
859 default:
860 NOT_REACHED();
861 }
862}
863
868void MidiFile::MoveFrom(MidiFile &other)
869{
870 std::swap(this->blocks, other.blocks);
871 std::swap(this->tempos, other.tempos);
872 this->tickdiv = other.tickdiv;
873
874 _midifile_instance = this;
875
876 other.blocks.clear();
877 other.tempos.clear();
878 other.tickdiv = 0;
879}
880
881static void WriteVariableLen(FileHandle &f, uint32_t value)
882{
883 if (value <= 0x7F) {
884 uint8_t tb = value;
885 fwrite(&tb, 1, 1, f);
886 } else if (value <= 0x3FFF) {
887 uint8_t tb[2];
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) {
892 uint8_t tb[3];
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) {
898 uint8_t tb[4];
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);
904 }
905}
906
912bool MidiFile::WriteSMF(const std::string &filename)
913{
914 auto of = FioFOpenFile(filename, "wb", Subdirectory::NO_DIRECTORY);
915 if (!of.has_value()) return false;
916 auto &f = *of;
917
918 /* SMF header */
919 const uint8_t fileheader[] = {
920 'M', 'T', 'h', 'd', // block name
921 0x00, 0x00, 0x00, 0x06, // BE32 block length, always 6 bytes
922 0x00, 0x00, // writing format 0 (all in one track)
923 0x00, 0x01, // containing 1 track (BE16)
924 (uint8_t)(this->tickdiv >> 8), (uint8_t)this->tickdiv, // tickdiv in BE16
925 };
926 fwrite(fileheader, sizeof(fileheader), 1, f);
927
928 /* Track header */
929 const uint8_t trackheader[] = {
930 'M', 'T', 'r', 'k', // block name
931 0, 0, 0, 0, // BE32 block length, unknown at this time
932 };
933 fwrite(trackheader, sizeof(trackheader), 1, f);
934 /* Determine position to write the actual track block length at */
935 size_t tracksizepos = ftell(f) - 4;
936
937 /* Write blocks in sequence */
938 uint32_t lasttime = 0;
939 size_t nexttempoindex = 0;
940 for (size_t bi = 0; bi < this->blocks.size(); bi++) {
941 DataBlock &block = this->blocks[bi];
942 TempoChange &nexttempo = this->tempos[nexttempoindex];
943
944 uint32_t timediff = block.ticktime - lasttime;
945
946 /* Check if there is a tempo change before this block */
947 if (nexttempo.ticktime < block.ticktime) {
948 timediff = nexttempo.ticktime - lasttime;
949 }
950
951 /* Write delta time for block */
952 lasttime += timediff;
953 bool needtime = false;
954 WriteVariableLen(f, timediff);
955
956 /* Write tempo change if there is one */
957 if (nexttempo.ticktime <= block.ticktime) {
958 uint8_t tempobuf[6] = { MIDIST_SMF_META, 0x51, 0x03, 0, 0, 0 };
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);
963 nexttempoindex++;
964 needtime = true;
965 }
966 /* If a tempo change occurred between two blocks, rather than
967 * at start of this one, start over with delta time for the block. */
968 if (nexttempo.ticktime < block.ticktime) {
969 /* Start loop over at same index */
970 bi--;
971 continue;
972 }
973
974 /* Write each block data command */
975 uint8_t *dp = block.data.data();
976 while (dp < block.data.data() + block.data.size()) {
977 /* Always zero delta time inside blocks */
978 if (needtime) {
979 fputc(0, f);
980 }
981 needtime = true;
982
983 /* Check message type and write appropriate number of bytes */
984 switch (*dp & 0xF0) {
985 case MIDIST_NOTEOFF:
986 case MIDIST_NOTEON:
987 case MIDIST_POLYPRESS:
988 case MIDIST_CONTROLLER:
989 case MIDIST_PITCHBEND:
990 fwrite(dp, 1, 3, f);
991 dp += 3;
992 continue;
993 case MIDIST_PROGCHG:
994 case MIDIST_CHANPRESS:
995 fwrite(dp, 1, 2, f);
996 dp += 2;
997 continue;
998 }
999
1000 /* Sysex needs to measure length and write that as well */
1001 if (*dp == MIDIST_SYSEX) {
1002 fwrite(dp, 1, 1, f);
1003 dp++;
1004 uint8_t *sysexend = dp;
1005 while (*sysexend != MIDIST_ENDSYSEX) sysexend++;
1006 ptrdiff_t sysexlen = sysexend - dp;
1007 WriteVariableLen(f, sysexlen);
1008 fwrite(dp, 1, sysexend - dp, f);
1009 dp = sysexend + 1;
1010 continue;
1011 }
1012
1013 /* Fail for any other commands */
1014 return false;
1015 }
1016 }
1017
1018 /* End of track marker */
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);
1021
1022 /* Fill out the RIFF block length */
1023 size_t trackendpos = ftell(f);
1024 fseek(f, tracksizepos, SEEK_SET);
1025 uint32_t tracksize = (uint32_t)(trackendpos - tracksizepos - 4); // blindly assume we never produce files larger than 2 GB
1026 tracksize = TO_BE32(tracksize);
1027 fwrite(&tracksize, 4, 1, f);
1028
1029 return true;
1030}
1031
1039std::string MidiFile::GetSMFFile(const MusicSongInfo &song)
1040{
1041 if (song.filetype == MTT_STANDARDMIDI) {
1042 std::string filename = FioFindFullPath(Subdirectory::BASESET_DIR, song.filename);
1043 if (!filename.empty()) return filename;
1045 if (!filename.empty()) return filename;
1046
1047 return std::string();
1048 }
1049
1050 if (song.filetype != MTT_MPSMIDI) return std::string();
1051
1052 std::string tempdirname = FioGetDirectory(Searchpath::SP_AUTODOWNLOAD_DIR, Subdirectory::BASESET_DIR);
1053 {
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);
1057
1058 /* Remove all '.' characters from filename */
1059 tempdirname.reserve(tempdirname.size() + basename.size());
1060 for (auto c : basename) {
1061 if (c != '.') tempdirname.append(1, c);
1062 }
1063 }
1064
1065 AppendPathSeparator(tempdirname);
1066 FioCreateDirectory(tempdirname);
1067
1068 std::string output_filename = fmt::format("{}{}.mid", tempdirname, song.cat_index);
1069
1070 if (FileExists(output_filename)) {
1071 /* If the file already exists, assume it's the correct decoded data */
1072 return output_filename;
1073 }
1074
1075 auto songdata = GetMusicCatEntryData(song.filename, song.cat_index);
1076 if (!songdata.has_value()) return std::string();
1077
1078 MidiFile midifile;
1079 if (!midifile.LoadMpsData(songdata->data(), songdata->size())) {
1080 return std::string();
1081 }
1082
1083 if (midifile.WriteSMF(output_filename)) {
1084 return output_filename;
1085 } else {
1086 return std::string();
1087 }
1088}
1089
1090
1091static bool CmdDumpSMF(std::span<std::string_view> argv)
1092{
1093 if (argv.empty()) {
1094 IConsolePrint(CC_HELP, "Write the current song to a Standard MIDI File. Usage: 'dumpsmf <filename>'.");
1095 return true;
1096 }
1097 if (argv.size() != 2) {
1098 IConsolePrint(CC_WARNING, "You must specify a filename to write MIDI data to.");
1099 return false;
1100 }
1101
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.");
1104 return false;
1105 }
1106
1107 std::string filename = fmt::format("{}{}", FiosGetScreenshotDir(), argv[1]);
1108 IConsolePrint(CC_INFO, "Dumping MIDI to '{}'.", filename);
1109
1110 if (_midifile_instance->WriteSMF(filename)) {
1111 IConsolePrint(CC_INFO, "File written successfully.");
1112 return true;
1113 } else {
1114 IConsolePrint(CC_ERROR, "An error occurred writing MIDI file.");
1115 return false;
1116 }
1117}
1118
1119static void RegisterConsoleMidiCommands()
1120{
1121 static bool registered = false;
1122 if (!registered) {
1123 IConsole::CmdRegister("dumpsmf", CmdDumpSMF);
1124 registered = true;
1125 }
1126}
1127
1128MidiFile::MidiFile()
1129{
1130 RegisterConsoleMidiCommands();
1131}
1132
1135{
1136 if (_midifile_instance == this) {
1137 _midifile_instance = nullptr;
1138 }
1139}
1140
Generic functions for replacing base data (graphics, sounds).
Generic functions for replacing base music data.
std::optional< std::vector< uint8_t > > GetMusicCatEntryData(const std::string &filename, size_t entrynum)
Read the full data of a music CAT file entry.
Definition music.cpp:50
@ MTT_MPSMIDI
MPS GM driver MIDI format (contained in a CAT file).
@ MTT_STANDARDMIDI
Standard MIDI file.
Owning byte buffer readable as a stream.
Definition midifile.cpp:68
ByteBuffer(FileHandle &file, size_t len)
Construct buffer from data in a file.
Definition midifile.cpp:79
bool IsValid() const
Return whether the buffer was constructed successfully.
Definition midifile.cpp:92
bool ReadByte(uint8_t &b)
Read a single byte from the buffer.
Definition midifile.cpp:111
bool IsEnd() const
Return whether reading has reached the end of the buffer.
Definition midifile.cpp:101
bool ReadVariableLength(uint32_t &res)
Read a MIDI file variable length value.
Definition midifile.cpp:125
bool ReadBuffer(uint8_t *dest, size_t length)
Read bytes into a buffer.
Definition midifile.cpp:143
bool Skip(size_t count)
Skip over a number of bytes in the buffer.
Definition midifile.cpp:172
bool ReadDataBlock(MidiFile::DataBlock *dest, size_t length)
Read bytes into a MidiFile::DataBlock.
Definition midifile.cpp:158
bool Rewind(size_t count)
Go a number of bytes back to re-read.
Definition midifile.cpp:185
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.
Definition console.cpp:90
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.
Definition engines.h:91
void AppendPathSeparator(std::string &buf)
Appends, if necessary, the path separator character to the end of the string.
Definition fileio.cpp:349
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.
Definition fileio.cpp:244
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 ...
Definition fileio.cpp:317
bool FileExists(std::string_view filename)
Test whether the given filename exists.
Definition fileio.cpp:132
std::string FioFindFullPath(Subdirectory subdir, std::string_view filename)
Find a path to the filename in one of the search directories.
Definition fileio.cpp:144
Functions for standard in/out file operations.
std::string_view FiosGetScreenshotDir()
Get the directory for screenshots.
Definition fios.cpp:579
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.
Definition fileio_type.h:94
@ BASESET_DIR
Subdirectory for all base data (base sets, intro game).
Definition fileio_type.h:96
Declarations for MIDI data.
MidiSysexMessage
Well-known MIDI system exclusive message values for use with the MidiGetStandardSysexMessage function...
Definition midi.h:142
@ ResetGM
Reset device to General MIDI defaults.
Definition midi.h:144
@ ResetGS
Reset device to (Roland) General Standard defaults.
Definition midi.h:146
@ ResetXG
Reset device to (Yamaha) XG defaults.
Definition midi.h:148
@ RolandSetReverb
Set up Roland SoundCanvas reverb room as TTD does.
Definition midi.h:150
@ MIDIST_SMF_META
only occurs in SMF data
Definition midi.h:49
@ MIDIST_SMF_ESCAPE
only occurs in SMF data
Definition midi.h:40
@ MIDIST_ENDSYSEX
only occurs in realtime data
Definition midi.h:39
const uint8_t * MidiGetStandardSysexMessage(MidiSysexMessage msg, size_t &length)
Retrieve a well-known MIDI system exclusive message.
Definition midifile.cpp:39
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.
Definition stdafx.h:271
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.
Definition console.cpp:138
std::vector< uint8_t > data
raw midi data contained in block
Definition midifile.hpp:22
uint32_t ticktime
tick number since start of file this block should be triggered at
Definition midifile.hpp:20
int64_t realtime
real-time (microseconds) since start of file this block should be triggered at
Definition midifile.hpp:21
uint32_t tempo
new tempo in microseconds per tick
Definition midifile.hpp:27
uint32_t ticktime
tick number since start of file this tempo change occurs at
Definition midifile.hpp:26
std::vector< TempoChange > tempos
list of tempo changes in file
Definition midifile.hpp:32
bool LoadMpsData(const uint8_t *data, size_t length)
Create MIDI data from song data for the original Microprose music drivers.
Definition midifile.cpp:836
void MoveFrom(MidiFile &other)
Move data from other to this, and clears other.
Definition midifile.cpp:868
bool LoadFile(const std::string &filename)
Load a standard MIDI file.
Definition midifile.cpp:452
static bool ReadSMFHeader(const std::string &filename, SMFHeader &header)
Read the header of a standard MIDI file.
Definition midifile.cpp:411
std::vector< DataBlock > blocks
sequential time-annotated data of file, merged to a single track
Definition midifile.hpp:31
~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
Definition midifile.hpp:33
bool WriteSMF(const std::string &filename)
Write a Standard MIDI File containing the decoded music.
Definition midifile.cpp:912
Starting parameter and playback status for one channel/track.
Definition midifile.cpp:508
uint8_t cur_program
program selected, used for velocity scaling (lookup into programvelocities array)
Definition midifile.cpp:509
uint16_t delay
frames until next command
Definition midifile.cpp:511
uint32_t playpos
next byte to play this channel from
Definition midifile.cpp:512
uint8_t running_status
last midi status code seen
Definition midifile.cpp:510
uint32_t startpos
start position of master track
Definition midifile.cpp:513
uint32_t returnpos
next return position after playing a segment
Definition midifile.cpp:514
Decoder for "MPS MIDI" format data.
Definition midifile.cpp:506
MpsMachine(const uint8_t *data, size_t length, MidiFile &target)
Construct a TTD DOS music format decoder.
Definition midifile.cpp:555
uint16_t PlayChannelFrame(MidiFile::DataBlock &outblock, int channel)
Play one frame of data from one channel.
Definition midifile.cpp:630
const uint8_t * songdata
raw data array
Definition midifile.cpp:526
int16_t initial_tempo
starting tempo of song
Definition midifile.cpp:520
uint16_t ReadVariableLength(uint32_t &pos)
Read an SMF-style variable length value (note duration) from songdata.
Definition midifile.cpp:594
static const uint8_t programvelocities[128]
Base note velocities for various GM programs.
Definition midifile.cpp:819
int16_t tempo_ticks
ticker that increments when playing a frame, decrements before playing a frame
Definition midifile.cpp:518
bool PlayFrame(MidiFile::DataBlock &block)
Play one frame of data into a block.
Definition midifile.cpp:757
MidiFile & target
recipient of data
Definition midifile.cpp:528
std::array< Channel, 16 > channels
playback status for each MIDI channel
Definition midifile.cpp:516
int16_t current_tempo
threshold for actually playing a frame
Definition midifile.cpp:519
std::vector< uint32_t > segments
pointers into songdata to repeatable data segments
Definition midifile.cpp:517
size_t songdatalen
length of song data
Definition midifile.cpp:527
static const int TEMPO_RATE
Frames/ticks per second for music playback.
Definition midifile.cpp:523
void RestartSong()
Prepare for playback from the beginning.
Definition midifile.cpp:608
MpsMidiStatus
Overridden MIDI status codes used in the data format.
Definition midifile.cpp:531
@ MPSMIDIST_SEGMENT_RETURN
resume playing master track from stored position
Definition midifile.cpp:532
@ MPSMIDIST_ENDSONG
immediately end the song
Definition midifile.cpp:534
@ MPSMIDIST_SEGMENT_CALL
store current position of master track playback, and begin playback of a segment
Definition midifile.cpp:533
bool shouldplayflag
not-end-of-song flag
Definition midifile.cpp:521
bool PlayInto()
Perform playback of whole song.
Definition midifile.cpp:784
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
Header of a Standard MIDI File.
Definition midi.h:14