OpenTTD Source: src/music/midifile.cpp Source File

OpenTTD Source 15.3

midifile.cpp
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
8/* @file midifile.cpp Parser for standard MIDI files */
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"
16#include "../core/endian_func.hpp"
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) {
47 case MidiSysexMessage::ResetGM:
48 length = lengthof(reset_gm_sysex);
49 return reset_gm_sysex;
50 case MidiSysexMessage::ResetGS:
51 length = lengthof(reset_gs_sysex);
52 return reset_gs_sysex;
53 case MidiSysexMessage::ResetXG:
54 length = lengthof(reset_xg_sysex);
55 return reset_xg_sysex;
56 case MidiSysexMessage::RolandSetReverb:
57 length = lengthof(roland_reverb_sysex);
58 return roland_reverb_sysex;
59 default:
60 NOT_REACHED();
61 }
62}
63
68 class ByteBuffer {
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
411 bool 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
426 bool MidiFile::ReadSMFHeader(FileHandle &file, SMFHeader &header)
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
452 bool 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
506 struct MpsMachine {
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;
528 MidiFile &target;
529
531 enum MpsMidiStatus : uint8_t {
532 MPSMIDIST_SEGMENT_RETURN = 0xFD,
533 MPSMIDIST_SEGMENT_CALL = 0xFE,
534 MPSMIDIST_ENDSONG = 0xFF,
535 };
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
608 void RestartSong()
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
627 uint16_t PlayChannelFrame(MidiFile::DataBlock &outblock, int channel)
628 {
629 uint16_t newdelay = 0;
630 uint8_t b1, b2;
631 Channel &chandata = this->channels[channel];
632
633 do {
634 /* Read command/status byte */
635 b1 = this->songdata[chandata.playpos++];
636
637 /* Command 0xFE, call segment from master track */
638 if (b1 == MPSMIDIST_SEGMENT_CALL) {
639 b1 = this->songdata[chandata.playpos++];
640 chandata.returnpos = chandata.playpos;
641 chandata.playpos = this->segments[b1];
642 newdelay = this->ReadVariableLength(chandata.playpos);
643 if (newdelay == 0) {
644 continue;
645 }
646 return newdelay;
647 }
648
649 /* Command 0xFD, return from segment to master track */
650 if (b1 == MPSMIDIST_SEGMENT_RETURN) {
651 chandata.playpos = chandata.returnpos;
652 chandata.returnpos = 0;
653 newdelay = this->ReadVariableLength(chandata.playpos);
654 if (newdelay == 0) {
655 continue;
656 }
657 return newdelay;
658 }
659
660 /* Command 0xFF, end of song */
661 if (b1 == MPSMIDIST_ENDSONG) {
662 this->shouldplayflag = false;
663 return 0;
664 }
665
666 /* Regular MIDI channel message status byte */
667 if (b1 >= 0x80) {
668 /* Save the status byte as running status for the channel
669 * and read another byte for first parameter to command */
670 chandata.running_status = b1;
671 b1 = this->songdata[chandata.playpos++];
672 }
673
674 switch (chandata.running_status & 0xF0) {
675 case MIDIST_NOTEOFF:
676 case MIDIST_NOTEON:
677 b2 = this->songdata[chandata.playpos++];
678 if (b2 != 0) {
679 /* Note on, read velocity and scale according to rules */
680 int16_t velocity;
681 if (channel == 9) {
682 /* Percussion channel, fixed velocity scaling not in the table */
683 velocity = (int16_t)b2 * 0x50;
684 } else {
685 /* Regular channel, use scaling from table */
686 velocity = b2 * programvelocities[chandata.cur_program];
687 }
688 b2 = (velocity / 128) & 0x00FF;
689 AddMidiData(outblock, MIDIST_NOTEON + channel, b1, b2);
690 } else {
691 /* Note off */
692 AddMidiData(outblock, MIDIST_NOTEON + channel, b1, 0);
693 }
694 break;
695 case MIDIST_CONTROLLER:
696 b2 = this->songdata[chandata.playpos++];
697 if (b1 == MIDICT_MODE_MONO) {
698 /* Unknown what the purpose of this is.
699 * Occurs in "Can't get There from Here" and in "Aliens Ate my Railway" a few times each.
700 * Possibly intended to give hints to other (non-GM) music drivers decoding the song.
701 */
702 break;
703 } else if (b1 == 0) {
704 /* Standard MIDI controller 0 is "bank select", override meaning to change tempo.
705 * This is not actually used in any of the original songs. */
706 if (b2 != 0) {
707 this->current_tempo = ((int)b2) * 48 / 60;
708 }
709 break;
710 } else if (b1 == MIDICT_EFFECTS1) {
711 /* Override value of this controller, default mapping is Reverb Send Level according to MMA RP-023.
712 * Unknown what the purpose of this particular value is. */
713 b2 = 30;
714 }
715 AddMidiData(outblock, MIDIST_CONTROLLER + channel, b1, b2);
716 break;
717 case MIDIST_PROGCHG:
718 if (b1 == 0x7E) {
719 /* Program change to "Applause" is originally used
720 * to cause the song to loop, but that gets handled
721 * separately in the output driver here.
722 * Just end the song. */
723 this->shouldplayflag = false;
724 break;
725 }
726 /* Used for note velocity scaling lookup */
727 chandata.cur_program = b1;
728 /* Two programs translated to a third, this is likely to
729 * provide three different velocity scalings of "brass". */
730 if (b1 == 0x57 || b1 == 0x3F) {
731 b1 = 0x3E;
732 }
733 AddMidiData(outblock, MIDIST_PROGCHG + channel, b1);
734 break;
735 case MIDIST_PITCHBEND:
736 b2 = this->songdata[chandata.playpos++];
737 AddMidiData(outblock, MIDIST_PITCHBEND + channel, b1, b2);
738 break;
739 default:
740 break;
741 }
742
743 newdelay = this->ReadVariableLength(chandata.playpos);
744 } while (newdelay == 0);
745
746 return newdelay;
747 }
748
752 bool PlayFrame(MidiFile::DataBlock &block)
753 {
754 /* Update tempo/ticks counter */
755 this->tempo_ticks -= this->current_tempo;
756 if (this->tempo_ticks > 0) {
757 return true;
758 }
759 this->tempo_ticks += TEMPO_RATE;
760
761 /* Look over all channels, play those active */
762 for (int ch = 0; ch < 16; ch++) {
763 Channel &chandata = this->channels[ch];
764 if (chandata.playpos != 0) {
765 if (chandata.delay == 0) {
766 chandata.delay = this->PlayChannelFrame(block, ch);
767 }
768 chandata.delay--;
769 }
770 }
771
772 return this->shouldplayflag;
773 }
774
778 bool PlayInto()
779 {
780 /* Tempo seems to be handled as TEMPO_RATE = 148 ticks per second.
781 * Use this as the tickdiv, and define the tempo to be somewhat less than one second (1M microseconds) per quarter note.
782 * This value was found experimentally to give a very close approximation of the correct playback speed.
783 * MIDI software loading exported files will show a bogus tempo, but playback will be correct. */
784 this->target.tickdiv = TEMPO_RATE;
785 this->target.tempos.push_back(MidiFile::TempoChange(0, 980500));
786
787 /* Initialize playback simulation */
788 this->RestartSong();
789 this->shouldplayflag = true;
790 this->current_tempo = (int32_t)this->initial_tempo * 24 / 60;
791 this->tempo_ticks = this->current_tempo;
792
793 /* Always reset percussion channel to program 0 */
794 auto &data_block = this->target.blocks.emplace_back();
795 AddMidiData(data_block, MIDIST_PROGCHG + 9, 0x00);
796
797 /* Technically should be an endless loop, but having
798 * a maximum (about 10 minutes) avoids getting stuck,
799 * in case of corrupted data. */
800 for (uint32_t tick = 0; tick < 100000; tick += 1) {
801 auto &block = this->target.blocks.emplace_back();
802 block.ticktime = tick;
803 if (!this->PlayFrame(block)) {
804 break;
805 }
806 }
807 return true;
808 }
809};
811const int MpsMachine::TEMPO_RATE = 148;
813 const uint8_t MpsMachine::programvelocities[128] = {
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,
822};
823
830 bool MidiFile::LoadMpsData(const uint8_t *data, size_t length)
831{
832 _midifile_instance = this;
833
834 MpsMachine machine(data, length, *this);
835 return machine.PlayInto() && FixupMidiData(*this);
836}
837
838bool MidiFile::LoadSong(const MusicSongInfo &song)
839{
840 switch (song.filetype) {
841 case MTT_STANDARDMIDI:
842 return this->LoadFile(song.filename);
843 case MTT_MPSMIDI:
844 {
845 auto songdata = GetMusicCatEntryData(song.filename, song.cat_index);
846 if (songdata.has_value()) {
847 bool result = this->LoadMpsData(songdata->data(), songdata->size());
848 return result;
849 } else {
850 return false;
851 }
852 }
853 default:
854 NOT_REACHED();
855 }
856}
857
862 void MidiFile::MoveFrom(MidiFile &other)
863{
864 std::swap(this->blocks, other.blocks);
865 std::swap(this->tempos, other.tempos);
866 this->tickdiv = other.tickdiv;
867
868 _midifile_instance = this;
869
870 other.blocks.clear();
871 other.tempos.clear();
872 other.tickdiv = 0;
873}
874
875static void WriteVariableLen(FileHandle &f, uint32_t value)
876{
877 if (value <= 0x7F) {
878 uint8_t tb = value;
879 fwrite(&tb, 1, 1, f);
880 } else if (value <= 0x3FFF) {
881 uint8_t tb[2];
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) {
886 uint8_t tb[3];
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) {
892 uint8_t tb[4];
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);
898 }
899}
900
906 bool MidiFile::WriteSMF(const std::string &filename)
907{
908 auto of = FioFOpenFile(filename, "wb", Subdirectory::NO_DIRECTORY);
909 if (!of.has_value()) return false;
910 auto &f = *of;
911
912 /* SMF header */
913 const uint8_t fileheader[] = {
914 'M', 'T', 'h', 'd', // block name
915 0x00, 0x00, 0x00, 0x06, // BE32 block length, always 6 bytes
916 0x00, 0x00, // writing format 0 (all in one track)
917 0x00, 0x01, // containing 1 track (BE16)
918 (uint8_t)(this->tickdiv >> 8), (uint8_t)this->tickdiv, // tickdiv in BE16
919 };
920 fwrite(fileheader, sizeof(fileheader), 1, f);
921
922 /* Track header */
923 const uint8_t trackheader[] = {
924 'M', 'T', 'r', 'k', // block name
925 0, 0, 0, 0, // BE32 block length, unknown at this time
926 };
927 fwrite(trackheader, sizeof(trackheader), 1, f);
928 /* Determine position to write the actual track block length at */
929 size_t tracksizepos = ftell(f) - 4;
930
931 /* Write blocks in sequence */
932 uint32_t lasttime = 0;
933 size_t nexttempoindex = 0;
934 for (size_t bi = 0; bi < this->blocks.size(); bi++) {
935 DataBlock &block = this->blocks[bi];
936 TempoChange &nexttempo = this->tempos[nexttempoindex];
937
938 uint32_t timediff = block.ticktime - lasttime;
939
940 /* Check if there is a tempo change before this block */
941 if (nexttempo.ticktime < block.ticktime) {
942 timediff = nexttempo.ticktime - lasttime;
943 }
944
945 /* Write delta time for block */
946 lasttime += timediff;
947 bool needtime = false;
948 WriteVariableLen(f, timediff);
949
950 /* Write tempo change if there is one */
951 if (nexttempo.ticktime <= block.ticktime) {
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);
957 nexttempoindex++;
958 needtime = true;
959 }
960 /* If a tempo change occurred between two blocks, rather than
961 * at start of this one, start over with delta time for the block. */
962 if (nexttempo.ticktime < block.ticktime) {
963 /* Start loop over at same index */
964 bi--;
965 continue;
966 }
967
968 /* Write each block data command */
969 uint8_t *dp = block.data.data();
970 while (dp < block.data.data() + block.data.size()) {
971 /* Always zero delta time inside blocks */
972 if (needtime) {
973 fputc(0, f);
974 }
975 needtime = true;
976
977 /* Check message type and write appropriate number of bytes */
978 switch (*dp & 0xF0) {
979 case MIDIST_NOTEOFF:
980 case MIDIST_NOTEON:
981 case MIDIST_POLYPRESS:
982 case MIDIST_CONTROLLER:
983 case MIDIST_PITCHBEND:
984 fwrite(dp, 1, 3, f);
985 dp += 3;
986 continue;
987 case MIDIST_PROGCHG:
988 case MIDIST_CHANPRESS:
989 fwrite(dp, 1, 2, f);
990 dp += 2;
991 continue;
992 }
993
994 /* Sysex needs to measure length and write that as well */
995 if (*dp == MIDIST_SYSEX) {
996 fwrite(dp, 1, 1, f);
997 dp++;
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);
1003 dp = sysexend + 1;
1004 continue;
1005 }
1006
1007 /* Fail for any other commands */
1008 return false;
1009 }
1010 }
1011
1012 /* End of track marker */
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);
1015
1016 /* Fill out the RIFF block length */
1017 size_t trackendpos = ftell(f);
1018 fseek(f, tracksizepos, SEEK_SET);
1019 uint32_t tracksize = (uint32_t)(trackendpos - tracksizepos - 4); // blindly assume we never produce files larger than 2 GB
1020 tracksize = TO_BE32(tracksize);
1021 fwrite(&tracksize, 4, 1, f);
1022
1023 return true;
1024}
1025
1033std::string MidiFile::GetSMFFile(const MusicSongInfo &song)
1034{
1035 if (song.filetype == MTT_STANDARDMIDI) {
1036 std::string filename = FioFindFullPath(Subdirectory::BASESET_DIR, song.filename);
1037 if (!filename.empty()) return filename;
1038 filename = FioFindFullPath(Subdirectory::OLD_GM_DIR, song.filename);
1039 if (!filename.empty()) return filename;
1040
1041 return std::string();
1042 }
1043
1044 if (song.filetype != MTT_MPSMIDI) return std::string();
1045
1046 std::string tempdirname = FioGetDirectory(Searchpath::SP_AUTODOWNLOAD_DIR, Subdirectory::BASESET_DIR);
1047 {
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);
1051
1052 /* Remove all '.' characters from filename */
1053 tempdirname.reserve(tempdirname.size() + basename.size());
1054 for (auto c : basename) {
1055 if (c != '.') tempdirname.append(1, c);
1056 }
1057 }
1058
1059 AppendPathSeparator(tempdirname);
1060 FioCreateDirectory(tempdirname);
1061
1062 std::string output_filename = fmt::format("{}{}.mid", tempdirname, song.cat_index);
1063
1064 if (FileExists(output_filename)) {
1065 /* If the file already exists, assume it's the correct decoded data */
1066 return output_filename;
1067 }
1068
1069 auto songdata = GetMusicCatEntryData(song.filename, song.cat_index);
1070 if (!songdata.has_value()) return std::string();
1071
1072 MidiFile midifile;
1073 if (!midifile.LoadMpsData(songdata->data(), songdata->size())) {
1074 return std::string();
1075 }
1076
1077 if (midifile.WriteSMF(output_filename)) {
1078 return output_filename;
1079 } else {
1080 return std::string();
1081 }
1082}
1083
1084
1085static bool CmdDumpSMF(std::span<std::string_view> argv)
1086{
1087 if (argv.empty()) {
1088 IConsolePrint(CC_HELP, "Write the current song to a Standard MIDI File. Usage: 'dumpsmf <filename>'.");
1089 return true;
1090 }
1091 if (argv.size() != 2) {
1092 IConsolePrint(CC_WARNING, "You must specify a filename to write MIDI data to.");
1093 return false;
1094 }
1095
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.");
1098 return false;
1099 }
1100
1101 std::string filename = fmt::format("{}{}", FiosGetScreenshotDir(), argv[1]);
1102 IConsolePrint(CC_INFO, "Dumping MIDI to '{}'.", filename);
1103
1104 if (_midifile_instance->WriteSMF(filename)) {
1105 IConsolePrint(CC_INFO, "File written successfully.");
1106 return true;
1107 } else {
1108 IConsolePrint(CC_ERROR, "An error occurred writing MIDI file.");
1109 return false;
1110 }
1111}
1112
1113static void RegisterConsoleMidiCommands()
1114{
1115 static bool registered = false;
1116 if (!registered) {
1117 IConsole::CmdRegister("dumpsmf", CmdDumpSMF);
1118 registered = true;
1119 }
1120}
1121
1122MidiFile::MidiFile()
1123{
1124 RegisterConsoleMidiCommands();
1125}
1126
1127MidiFile::~MidiFile()
1128{
1129 if (_midifile_instance == this) {
1130 _midifile_instance = nullptr;
1131 }
1132}
1133
GetMusicCatEntryData
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
@ MTT_MPSMIDI
MPS GM driver MIDI format (contained in a CAT file)
Definition base_media_music.h:31
MTT_STANDARDMIDI
@ MTT_STANDARDMIDI
Standard MIDI file.
Definition base_media_music.h:30
ByteBuffer
Owning byte buffer readable as a stream.
Definition midifile.cpp:68
ByteBuffer::ByteBuffer
ByteBuffer(FileHandle &file, size_t len)
Construct buffer from data in a file.
Definition midifile.cpp:79
ByteBuffer::IsValid
bool IsValid() const
Return whether the buffer was constructed successfully.
Definition midifile.cpp:92
ByteBuffer::ReadByte
bool ReadByte(uint8_t &b)
Read a single byte from the buffer.
Definition midifile.cpp:111
ByteBuffer::IsEnd
bool IsEnd() const
Return whether reading has reached the end of the buffer.
Definition midifile.cpp:101
ByteBuffer::ReadVariableLength
bool ReadVariableLength(uint32_t &res)
Read a MIDI file variable length value.
Definition midifile.cpp:125
ByteBuffer::ReadBuffer
bool ReadBuffer(uint8_t *dest, size_t length)
Read bytes into a buffer.
Definition midifile.cpp:143
ByteBuffer::Skip
bool Skip(size_t count)
Skip over a number of bytes in the buffer.
Definition midifile.cpp:172
ByteBuffer::ReadDataBlock
bool ReadDataBlock(MidiFile::DataBlock *dest, size_t length)
Read bytes into a MidiFile::DataBlock.
Definition midifile.cpp:158
ByteBuffer::Rewind
bool Rewind(size_t count)
Go a number of bytes back to re-read.
Definition midifile.cpp:185
FileHandle
Definition fileio_type.h:132
IConsolePrint
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
CC_HELP
static const TextColour CC_HELP
Colour for help lines.
Definition console_type.h:26
CC_INFO
static const TextColour CC_INFO
Colour for information lines.
Definition console_type.h:27
CC_WARNING
static const TextColour CC_WARNING
Colour for warning lines.
Definition console_type.h:25
CC_ERROR
static const TextColour CC_ERROR
Colour for error lines.
Definition console_type.h:24
AppendPathSeparator
void AppendPathSeparator(std::string &buf)
Appends, if necessary, the path separator character to the end of the string.
Definition fileio.cpp:348
FioFOpenFile
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:242
FioCreateDirectory
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:315
FileExists
bool FileExists(std::string_view filename)
Test whether the given filename exists.
Definition fileio.cpp:132
FioFindFullPath
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
FiosGetScreenshotDir
std::string_view FiosGetScreenshotDir()
Get the directory for screenshots.
Definition fios.cpp:581
SP_AUTODOWNLOAD_DIR
@ SP_AUTODOWNLOAD_DIR
Search within the autodownload directory.
Definition fileio_type.h:124
NO_DIRECTORY
@ NO_DIRECTORY
A path without any base directory.
Definition fileio_type.h:107
OLD_GM_DIR
@ OLD_GM_DIR
Old subdirectory for the music.
Definition fileio_type.h:94
BASESET_DIR
@ BASESET_DIR
Subdirectory for all base data (base sets, intro game)
Definition fileio_type.h:96
lengthof
#define lengthof(array)
Return the length of an fixed size array.
Definition stdafx.h:271
IConsole::CmdRegister
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:137
MidiFile::DataBlock
Definition midifile.hpp:19
MidiFile::DataBlock::data
std::vector< uint8_t > data
raw midi data contained in block
Definition midifile.hpp:22
MidiFile::DataBlock::ticktime
uint32_t ticktime
tick number since start of file this block should be triggered at
Definition midifile.hpp:20
MidiFile::DataBlock::realtime
int64_t realtime
real-time (microseconds) since start of file this block should be triggered at
Definition midifile.hpp:21
MidiFile::TempoChange
Definition midifile.hpp:25
MidiFile::TempoChange::tempo
uint32_t tempo
new tempo in microseconds per tick
Definition midifile.hpp:27
MidiFile::TempoChange::ticktime
uint32_t ticktime
tick number since start of file this tempo change occurs at
Definition midifile.hpp:26
MidiFile
Definition midifile.hpp:18
MidiFile::tempos
std::vector< TempoChange > tempos
list of tempo changes in file
Definition midifile.hpp:32
MidiFile::LoadMpsData
bool LoadMpsData(const uint8_t *data, size_t length)
Create MIDI data from song data for the original Microprose music drivers.
Definition midifile.cpp:830
MidiFile::MoveFrom
void MoveFrom(MidiFile &other)
Move data from other to this, and clears other.
Definition midifile.cpp:862
MidiFile::LoadFile
bool LoadFile(const std::string &filename)
Load a standard MIDI file.
Definition midifile.cpp:452
MidiFile::ReadSMFHeader
static bool ReadSMFHeader(const std::string &filename, SMFHeader &header)
Read the header of a standard MIDI file.
Definition midifile.cpp:411
MidiFile::blocks
std::vector< DataBlock > blocks
sequential time-annotated data of file, merged to a single track
Definition midifile.hpp:31
MidiFile::GetSMFFile
static std::string GetSMFFile(const MusicSongInfo &song)
Get the name of a Standard MIDI File for a given song.
Definition midifile.cpp:1033
MidiFile::tickdiv
uint16_t tickdiv
ticks per quarter note
Definition midifile.hpp:33
MidiFile::WriteSMF
bool WriteSMF(const std::string &filename)
Write a Standard MIDI File containing the decoded music.
Definition midifile.cpp:906
MpsMachine::Channel
Starting parameter and playback status for one channel/track.
Definition midifile.cpp:508
MpsMachine::Channel::cur_program
uint8_t cur_program
program selected, used for velocity scaling (lookup into programvelocities array)
Definition midifile.cpp:509
MpsMachine::Channel::delay
uint16_t delay
frames until next command
Definition midifile.cpp:511
MpsMachine::Channel::playpos
uint32_t playpos
next byte to play this channel from
Definition midifile.cpp:512
MpsMachine::Channel::running_status
uint8_t running_status
last midi status code seen
Definition midifile.cpp:510
MpsMachine::Channel::startpos
uint32_t startpos
start position of master track
Definition midifile.cpp:513
MpsMachine::Channel::returnpos
uint32_t returnpos
next return position after playing a segment
Definition midifile.cpp:514
MpsMachine
Decoder for "MPS MIDI" format data.
Definition midifile.cpp:506
MpsMachine::MpsMachine
MpsMachine(const uint8_t *data, size_t length, MidiFile &target)
Construct a TTD DOS music format decoder.
Definition midifile.cpp:555
MpsMachine::PlayChannelFrame
uint16_t PlayChannelFrame(MidiFile::DataBlock &outblock, int channel)
Play one frame of data from one channel.
Definition midifile.cpp:627
MpsMachine::songdata
const uint8_t * songdata
raw data array
Definition midifile.cpp:526
MpsMachine::initial_tempo
int16_t initial_tempo
starting tempo of song
Definition midifile.cpp:520
MpsMachine::ReadVariableLength
uint16_t ReadVariableLength(uint32_t &pos)
Read an SMF-style variable length value (note duration) from songdata.
Definition midifile.cpp:594
MpsMachine::programvelocities
static const uint8_t programvelocities[128]
Base note velocities for various GM programs.
Definition midifile.cpp:813
MpsMachine::tempo_ticks
int16_t tempo_ticks
ticker that increments when playing a frame, decrements before playing a frame
Definition midifile.cpp:518
MpsMachine::PlayFrame
bool PlayFrame(MidiFile::DataBlock &block)
Play one frame of data into a block.
Definition midifile.cpp:752
MpsMachine::target
MidiFile & target
recipient of data
Definition midifile.cpp:528
MpsMachine::channels
std::array< Channel, 16 > channels
playback status for each MIDI channel
Definition midifile.cpp:516
MpsMachine::current_tempo
int16_t current_tempo
threshold for actually playing a frame
Definition midifile.cpp:519
MpsMachine::segments
std::vector< uint32_t > segments
pointers into songdata to repeatable data segments
Definition midifile.cpp:517
MpsMachine::songdatalen
size_t songdatalen
length of song data
Definition midifile.cpp:527
MpsMachine::TEMPO_RATE
static const int TEMPO_RATE
Frames/ticks per second for music playback.
Definition midifile.cpp:523
MpsMachine::RestartSong
void RestartSong()
Prepare for playback from the beginning.
Definition midifile.cpp:608
MpsMachine::MpsMidiStatus
MpsMidiStatus
Overridden MIDI status codes used in the data format.
Definition midifile.cpp:531
MpsMachine::MPSMIDIST_SEGMENT_RETURN
@ MPSMIDIST_SEGMENT_RETURN
resume playing master track from stored position
Definition midifile.cpp:532
MpsMachine::MPSMIDIST_ENDSONG
@ MPSMIDIST_ENDSONG
immediately end the song
Definition midifile.cpp:534
MpsMachine::MPSMIDIST_SEGMENT_CALL
@ MPSMIDIST_SEGMENT_CALL
store current position of master track playback, and begin playback of a segment
Definition midifile.cpp:533
MpsMachine::shouldplayflag
bool shouldplayflag
not-end-of-song flag
Definition midifile.cpp:521
MpsMachine::PlayInto
bool PlayInto()
Perform playback of whole song.
Definition midifile.cpp:778
MusicSongInfo
Metadata about a music track.
Definition base_media_music.h:35
MusicSongInfo::filetype
MusicTrackType filetype
decoder required for song file
Definition base_media_music.h:42
MusicSongInfo::filename
std::string filename
file on disk containing song (when used in MusicSet class)
Definition base_media_music.h:37
MusicSongInfo::cat_index
int cat_index
entry index in CAT file, for filetype==MTT_MPSMIDI
Definition base_media_music.h:38
SMFHeader
Header of a Standard MIDI File.
Definition midi.h:14