OpenTTD Source 20241224-master-gf74b0cf984
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 <http://www.gnu.org/licenses/>.
6 */
7
8/* @file midifile.cpp Parser for standard MIDI files */
9
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 "../core/mem_func.hpp"
16#include "../base_media_base.h"
17#include "midi.h"
18
19#include "../console_func.h"
20#include "../console_internal.h"
21
22/* SMF reader based on description at: http://www.somascape.org/midi/tech/mfile.html */
23
24
25static MidiFile *_midifile_instance = nullptr;
26
33const uint8_t *MidiGetStandardSysexMessage(MidiSysexMessage msg, size_t &length)
34{
35 static uint8_t reset_gm_sysex[] = { 0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7 };
36 static uint8_t reset_gs_sysex[] = { 0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7 };
37 static uint8_t reset_xg_sysex[] = { 0xF0, 0x43, 0x10, 0x4C, 0x00, 0x00, 0x7E, 0x00, 0xF7 };
38 static uint8_t roland_reverb_sysex[] = { 0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x01, 0x30, 0x02, 0x04, 0x00, 0x40, 0x40, 0x00, 0x00, 0x09, 0xF7 };
39
40 switch (msg) {
41 case MidiSysexMessage::ResetGM:
42 length = lengthof(reset_gm_sysex);
43 return reset_gm_sysex;
44 case MidiSysexMessage::ResetGS:
45 length = lengthof(reset_gs_sysex);
46 return reset_gs_sysex;
47 case MidiSysexMessage::ResetXG:
48 length = lengthof(reset_xg_sysex);
49 return reset_xg_sysex;
50 case MidiSysexMessage::RolandSetReverb:
51 length = lengthof(roland_reverb_sysex);
52 return roland_reverb_sysex;
53 default:
54 NOT_REACHED();
55 }
56}
57
63 std::vector<uint8_t> buf;
64 size_t pos;
65public:
73 ByteBuffer(FileHandle &file, size_t len)
74 {
75 this->buf.resize(len);
76 if (fread(this->buf.data(), 1, len, file) == len) {
77 this->pos = 0;
78 } else {
79 /* invalid state */
80 this->buf.clear();
81 }
82 }
83
88 bool IsValid() const
89 {
90 return !this->buf.empty();
91 }
92
97 bool IsEnd() const
98 {
99 return this->pos >= this->buf.size();
100 }
101
107 bool ReadByte(uint8_t &b)
108 {
109 if (this->IsEnd()) return false;
110 b = this->buf[this->pos++];
111 return true;
112 }
113
121 bool ReadVariableLength(uint32_t &res)
122 {
123 res = 0;
124 uint8_t b = 0;
125 do {
126 if (this->IsEnd()) return false;
127 b = this->buf[this->pos++];
128 res = (res << 7) | (b & 0x7F);
129 } while (b & 0x80);
130 return true;
131 }
132
139 bool ReadBuffer(uint8_t *dest, size_t length)
140 {
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);
144 this->pos += length;
145 return true;
146 }
147
154 bool ReadDataBlock(MidiFile::DataBlock *dest, size_t length)
155 {
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);
159 this->pos += length;
160 return true;
161 }
162
168 bool Skip(size_t count)
169 {
170 if (this->IsEnd()) return false;
171 if (this->buf.size() - this->pos < count) return false;
172 this->pos += count;
173 return true;
174 }
175
181 bool Rewind(size_t count)
182 {
183 if (count > this->pos) return false;
184 this->pos -= count;
185 return true;
186 }
187};
188
189static bool ReadTrackChunk(FileHandle &file, MidiFile &target)
190{
191 uint8_t buf[4];
192
193 const uint8_t magic[] = { 'M', 'T', 'r', 'k' };
194 if (fread(buf, sizeof(magic), 1, file) != 1) {
195 return false;
196 }
197 if (memcmp(magic, buf, sizeof(magic)) != 0) {
198 return false;
199 }
200
201 /* Read chunk length and then the whole chunk */
202 uint32_t chunk_length;
203 if (fread(&chunk_length, 1, 4, file) != 4) {
204 return false;
205 }
206 chunk_length = FROM_BE32(chunk_length);
207
208 ByteBuffer chunk(file, chunk_length);
209 if (!chunk.IsValid()) {
210 return false;
211 }
212
213 MidiFile::DataBlock *block = &target.blocks.emplace_back();
214
215 uint8_t last_status = 0;
216 bool running_sysex = false;
217 while (!chunk.IsEnd()) {
218 /* Read deltatime for event, start new block */
219 uint32_t deltatime = 0;
220 if (!chunk.ReadVariableLength(deltatime)) {
221 return false;
222 }
223 if (deltatime > 0) {
224 block = &target.blocks.emplace_back(block->ticktime + deltatime);
225 }
226
227 /* Read status byte */
228 uint8_t status;
229 if (!chunk.ReadByte(status)) {
230 return false;
231 }
232
233 if ((status & 0x80) == 0) {
234 /* High bit not set means running status message, status is same as last
235 * convert to explicit status */
236 chunk.Rewind(1);
237 status = last_status;
238 goto running_status;
239 } else if ((status & 0xF0) != 0xF0) {
240 /* Regular channel message */
241 last_status = status;
242 running_status:
243 switch (status & 0xF0) {
244 case MIDIST_NOTEOFF:
245 case MIDIST_NOTEON:
246 case MIDIST_POLYPRESS:
247 case MIDIST_CONTROLLER:
248 case MIDIST_PITCHBEND:
249 /* 3 byte messages */
250 block->data.push_back(status);
251 if (!chunk.ReadDataBlock(block, 2)) {
252 return false;
253 }
254 break;
255 case MIDIST_PROGCHG:
256 case MIDIST_CHANPRESS:
257 /* 2 byte messages */
258 block->data.push_back(status);
259 if (!chunk.ReadByte(buf[0])) {
260 return false;
261 }
262 block->data.push_back(buf[0]);
263 break;
264 default:
265 NOT_REACHED();
266 }
267 } else if (status == MIDIST_SMF_META) {
268 /* Meta event, read event type byte and data length */
269 if (!chunk.ReadByte(buf[0])) {
270 return false;
271 }
272 uint32_t length = 0;
273 if (!chunk.ReadVariableLength(length)) {
274 return false;
275 }
276 switch (buf[0]) {
277 case 0x2F:
278 /* end of track, no more data (length != 0 is illegal) */
279 return (length == 0);
280 case 0x51:
281 /* tempo change */
282 if (length != 3) return false;
283 if (!chunk.ReadBuffer(buf, 3)) return false;
284 target.tempos.push_back(MidiFile::TempoChange(block->ticktime, buf[0] << 16 | buf[1] << 8 | buf[2]));
285 break;
286 default:
287 /* unimportant meta event, skip over it */
288 if (!chunk.Skip(length)) {
289 return false;
290 }
291 break;
292 }
293 } else if (status == MIDIST_SYSEX || (status == MIDIST_SMF_ESCAPE && running_sysex)) {
294 /* System exclusive message */
295 uint32_t length = 0;
296 if (!chunk.ReadVariableLength(length)) {
297 return false;
298 }
299 block->data.push_back(0xF0);
300 if (!chunk.ReadDataBlock(block, length)) {
301 return false;
302 }
303 if (block->data.back() != 0xF7) {
304 /* Engage Casio weirdo mode - convert to normal sysex */
305 running_sysex = true;
306 block->data.push_back(0xF7);
307 } else {
308 running_sysex = false;
309 }
310 } else if (status == MIDIST_SMF_ESCAPE) {
311 /* Escape sequence */
312 uint32_t length = 0;
313 if (!chunk.ReadVariableLength(length)) {
314 return false;
315 }
316 if (!chunk.ReadDataBlock(block, length)) {
317 return false;
318 }
319 } else {
320 /* Messages undefined in standard midi files:
321 * 0xF1 - MIDI time code quarter frame
322 * 0xF2 - Song position pointer
323 * 0xF3 - Song select
324 * 0xF4 - undefined/reserved
325 * 0xF5 - undefined/reserved
326 * 0xF6 - Tune request for analog synths
327 * 0xF8..0xFE - System real-time messages
328 */
329 return false;
330 }
331 }
332
333 NOT_REACHED();
334}
335
336template<typename T>
337bool TicktimeAscending(const T &a, const T &b)
338{
339 return a.ticktime < b.ticktime;
340}
341
342static bool FixupMidiData(MidiFile &target)
343{
344 /* Sort all tempo changes and events */
345 std::sort(target.tempos.begin(), target.tempos.end(), TicktimeAscending<MidiFile::TempoChange>);
346 std::sort(target.blocks.begin(), target.blocks.end(), TicktimeAscending<MidiFile::DataBlock>);
347
348 if (target.tempos.empty()) {
349 /* No tempo information, assume 120 bpm (500,000 microseconds per beat */
350 target.tempos.push_back(MidiFile::TempoChange(0, 500000));
351 }
352 /* Add sentinel tempo at end */
353 target.tempos.push_back(MidiFile::TempoChange(UINT32_MAX, 0));
354
355 /* Merge blocks with identical tick times */
356 std::vector<MidiFile::DataBlock> merged_blocks;
357 uint32_t last_ticktime = 0;
358 for (size_t i = 0; i < target.blocks.size(); i++) {
359 MidiFile::DataBlock &block = target.blocks[i];
360 if (block.data.empty()) {
361 continue;
362 } else if (block.ticktime > last_ticktime || merged_blocks.empty()) {
363 merged_blocks.push_back(block);
364 last_ticktime = block.ticktime;
365 } else {
366 merged_blocks.back().data.insert(merged_blocks.back().data.end(), block.data.begin(), block.data.end());
367 }
368 }
369 std::swap(merged_blocks, target.blocks);
370
371 /* Annotate blocks with real time */
372 last_ticktime = 0;
373 uint32_t last_realtime = 0;
374 size_t cur_tempo = 0, cur_block = 0;
375 while (cur_block < target.blocks.size()) {
376 MidiFile::DataBlock &block = target.blocks[cur_block];
377 MidiFile::TempoChange &tempo = target.tempos[cur_tempo];
378 MidiFile::TempoChange &next_tempo = target.tempos[cur_tempo + 1];
379 if (block.ticktime <= next_tempo.ticktime) {
380 /* block is within the current tempo */
381 int64_t tickdiff = block.ticktime - last_ticktime;
382 last_ticktime = block.ticktime;
383 last_realtime += uint32_t(tickdiff * tempo.tempo / target.tickdiv);
384 block.realtime = last_realtime;
385 cur_block++;
386 } else {
387 /* tempo change occurs before this block */
388 int64_t tickdiff = next_tempo.ticktime - last_ticktime;
389 last_ticktime = next_tempo.ticktime;
390 last_realtime += uint32_t(tickdiff * tempo.tempo / target.tickdiv); // current tempo until the tempo change
391 cur_tempo++;
392 }
393 }
394
395 return true;
396}
397
404bool MidiFile::ReadSMFHeader(const std::string &filename, SMFHeader &header)
405{
406 auto file = FioFOpenFile(filename, "rb", Subdirectory::BASESET_DIR);
407 if (!file.has_value()) return false;
408 bool result = ReadSMFHeader(*file, header);
409 return result;
410}
411
420{
421 /* Try to read header, fixed size */
422 uint8_t buffer[14];
423 if (fread(buffer, sizeof(buffer), 1, file) != 1) {
424 return false;
425 }
426
427 /* Check magic, 'MThd' followed by 4 byte length indicator (always = 6 in SMF) */
428 const uint8_t magic[] = { 'M', 'T', 'h', 'd', 0x00, 0x00, 0x00, 0x06 };
429 if (MemCmpT(buffer, magic, sizeof(magic)) != 0) {
430 return false;
431 }
432
433 /* Read the parameters of the file */
434 header.format = (buffer[8] << 8) | buffer[9];
435 header.tracks = (buffer[10] << 8) | buffer[11];
436 header.tickdiv = (buffer[12] << 8) | buffer[13];
437 return true;
438}
439
445bool MidiFile::LoadFile(const std::string &filename)
446{
447 _midifile_instance = this;
448
449 this->blocks.clear();
450 this->tempos.clear();
451 this->tickdiv = 0;
452
453 auto file = FioFOpenFile(filename, "rb", Subdirectory::BASESET_DIR);
454 if (!file.has_value()) return false;
455
456 SMFHeader header;
457 if (!ReadSMFHeader(*file, header)) return false;
458
459 /* Only format 0 (single-track) and format 1 (multi-track single-song) are accepted for now */
460 if (header.format != 0 && header.format != 1) return false;
461 /* Doesn't support SMPTE timecode files */
462 if ((header.tickdiv & 0x8000) != 0) return false;
463
464 this->tickdiv = header.tickdiv;
465
466 for (; header.tracks > 0; header.tracks--) {
467 if (!ReadTrackChunk(*file, *this)) {
468 return false;
469 }
470 }
471
472 return FixupMidiData(*this);
473}
474
475
499 struct Channel {
500 uint8_t cur_program;
502 uint16_t delay;
503 uint32_t playpos;
504 uint32_t startpos;
505 uint32_t returnpos;
506 Channel() : cur_program(0xFF), running_status(0), delay(0), playpos(0), startpos(0), returnpos(0) { }
507 };
509 std::vector<uint32_t> segments;
510 int16_t tempo_ticks;
514
515 static const int TEMPO_RATE;
516 static const uint8_t programvelocities[128];
517
518 const uint8_t *songdata;
519 size_t songdatalen;
521
528
529 static void AddMidiData(MidiFile::DataBlock &block, uint8_t b1, uint8_t b2)
530 {
531 block.data.push_back(b1);
532 block.data.push_back(b2);
533 }
534 static void AddMidiData(MidiFile::DataBlock &block, uint8_t b1, uint8_t b2, uint8_t b3)
535 {
536 block.data.push_back(b1);
537 block.data.push_back(b2);
538 block.data.push_back(b3);
539 }
540
547 MpsMachine(const uint8_t *data, size_t length, MidiFile &target)
548 : songdata(data), songdatalen(length), target(target)
549 {
550 uint32_t pos = 0;
551 int loopmax;
552 int loopidx;
553
554 /* First byte is the initial "tempo" */
555 this->initial_tempo = this->songdata[pos++];
556
557 /* Next byte is a count of callable segments */
558 loopmax = this->songdata[pos++];
559 for (loopidx = 0; loopidx < loopmax; loopidx++) {
560 /* Segments form a linked list in the stream,
561 * first two bytes in each is an offset to the next.
562 * Two bytes between offset to next and start of data
563 * are unaccounted for. */
564 this->segments.push_back(pos + 4);
565 pos += FROM_LE16(*(const int16_t *)(this->songdata + pos));
566 }
567
568 /* After segments follows list of master tracks for each channel,
569 * also prefixed with a byte counting actual tracks. */
570 loopmax = this->songdata[pos++];
571 for (loopidx = 0; loopidx < loopmax; loopidx++) {
572 /* Similar structure to segments list, but also has
573 * the MIDI channel number as a byte before the offset
574 * to next track. */
575 uint8_t ch = this->songdata[pos++];
576 this->channels[ch].startpos = pos + 4;
577 pos += FROM_LE16(*(const int16_t *)(this->songdata + pos));
578 }
579 }
580
586 uint16_t ReadVariableLength(uint32_t &pos)
587 {
588 uint8_t b = 0;
589 uint16_t res = 0;
590 do {
591 b = this->songdata[pos++];
592 res = (res << 7) + (b & 0x7F);
593 } while (b & 0x80);
594 return res;
595 }
596
601 {
602 for (int ch = 0; ch < 16; ch++) {
603 Channel &chandata = this->channels[ch];
604 if (chandata.startpos != 0) {
605 /* Active track, set position to beginning */
606 chandata.playpos = chandata.startpos;
607 chandata.delay = this->ReadVariableLength(chandata.playpos);
608 } else {
609 /* Inactive track, mark as such */
610 chandata.playpos = 0;
611 chandata.delay = 0;
612 }
613 }
614 }
615
619 uint16_t PlayChannelFrame(MidiFile::DataBlock &outblock, int channel)
620 {
621 uint16_t newdelay = 0;
622 uint8_t b1, b2;
623 Channel &chandata = this->channels[channel];
624
625 do {
626 /* Read command/status byte */
627 b1 = this->songdata[chandata.playpos++];
628
629 /* Command 0xFE, call segment from master track */
630 if (b1 == MPSMIDIST_SEGMENT_CALL) {
631 b1 = this->songdata[chandata.playpos++];
632 chandata.returnpos = chandata.playpos;
633 chandata.playpos = this->segments[b1];
634 newdelay = this->ReadVariableLength(chandata.playpos);
635 if (newdelay == 0) {
636 continue;
637 }
638 return newdelay;
639 }
640
641 /* Command 0xFD, return from segment to master track */
642 if (b1 == MPSMIDIST_SEGMENT_RETURN) {
643 chandata.playpos = chandata.returnpos;
644 chandata.returnpos = 0;
645 newdelay = this->ReadVariableLength(chandata.playpos);
646 if (newdelay == 0) {
647 continue;
648 }
649 return newdelay;
650 }
651
652 /* Command 0xFF, end of song */
653 if (b1 == MPSMIDIST_ENDSONG) {
654 this->shouldplayflag = false;
655 return 0;
656 }
657
658 /* Regular MIDI channel message status byte */
659 if (b1 >= 0x80) {
660 /* Save the status byte as running status for the channel
661 * and read another byte for first parameter to command */
662 chandata.running_status = b1;
663 b1 = this->songdata[chandata.playpos++];
664 }
665
666 switch (chandata.running_status & 0xF0) {
667 case MIDIST_NOTEOFF:
668 case MIDIST_NOTEON:
669 b2 = this->songdata[chandata.playpos++];
670 if (b2 != 0) {
671 /* Note on, read velocity and scale according to rules */
672 int16_t velocity;
673 if (channel == 9) {
674 /* Percussion channel, fixed velocity scaling not in the table */
675 velocity = (int16_t)b2 * 0x50;
676 } else {
677 /* Regular channel, use scaling from table */
678 velocity = b2 * programvelocities[chandata.cur_program];
679 }
680 b2 = (velocity / 128) & 0x00FF;
681 AddMidiData(outblock, MIDIST_NOTEON + channel, b1, b2);
682 } else {
683 /* Note off */
684 AddMidiData(outblock, MIDIST_NOTEON + channel, b1, 0);
685 }
686 break;
687 case MIDIST_CONTROLLER:
688 b2 = this->songdata[chandata.playpos++];
689 if (b1 == MIDICT_MODE_MONO) {
690 /* Unknown what the purpose of this is.
691 * Occurs in "Can't get There from Here" and in "Aliens Ate my Railway" a few times each.
692 * Possibly intended to give hints to other (non-GM) music drivers decoding the song.
693 */
694 break;
695 } else if (b1 == 0) {
696 /* Standard MIDI controller 0 is "bank select", override meaning to change tempo.
697 * This is not actually used in any of the original songs. */
698 if (b2 != 0) {
699 this->current_tempo = ((int)b2) * 48 / 60;
700 }
701 break;
702 } else if (b1 == MIDICT_EFFECTS1) {
703 /* Override value of this controller, default mapping is Reverb Send Level according to MMA RP-023.
704 * Unknown what the purpose of this particular value is. */
705 b2 = 30;
706 }
707 AddMidiData(outblock, MIDIST_CONTROLLER + channel, b1, b2);
708 break;
709 case MIDIST_PROGCHG:
710 if (b1 == 0x7E) {
711 /* Program change to "Applause" is originally used
712 * to cause the song to loop, but that gets handled
713 * separately in the output driver here.
714 * Just end the song. */
715 this->shouldplayflag = false;
716 break;
717 }
718 /* Used for note velocity scaling lookup */
719 chandata.cur_program = b1;
720 /* Two programs translated to a third, this is likely to
721 * provide three different velocity scalings of "brass". */
722 if (b1 == 0x57 || b1 == 0x3F) {
723 b1 = 0x3E;
724 }
725 AddMidiData(outblock, MIDIST_PROGCHG + channel, b1);
726 break;
727 case MIDIST_PITCHBEND:
728 b2 = this->songdata[chandata.playpos++];
729 AddMidiData(outblock, MIDIST_PITCHBEND + channel, b1, b2);
730 break;
731 default:
732 break;
733 }
734
735 newdelay = this->ReadVariableLength(chandata.playpos);
736 } while (newdelay == 0);
737
738 return newdelay;
739 }
740
745 {
746 /* Update tempo/ticks counter */
747 this->tempo_ticks -= this->current_tempo;
748 if (this->tempo_ticks > 0) {
749 return true;
750 }
751 this->tempo_ticks += TEMPO_RATE;
752
753 /* Look over all channels, play those active */
754 for (int ch = 0; ch < 16; ch++) {
755 Channel &chandata = this->channels[ch];
756 if (chandata.playpos != 0) {
757 if (chandata.delay == 0) {
758 chandata.delay = this->PlayChannelFrame(block, ch);
759 }
760 chandata.delay--;
761 }
762 }
763
764 return this->shouldplayflag;
765 }
766
770 bool PlayInto()
771 {
772 /* Tempo seems to be handled as TEMPO_RATE = 148 ticks per second.
773 * Use this as the tickdiv, and define the tempo to be somewhat less than one second (1M microseconds) per quarter note.
774 * This value was found experimentally to give a very close approximation of the correct playback speed.
775 * MIDI software loading exported files will show a bogus tempo, but playback will be correct. */
776 this->target.tickdiv = TEMPO_RATE;
777 this->target.tempos.push_back(MidiFile::TempoChange(0, 980500));
778
779 /* Initialize playback simulation */
780 this->RestartSong();
781 this->shouldplayflag = true;
782 this->current_tempo = (int32_t)this->initial_tempo * 24 / 60;
783 this->tempo_ticks = this->current_tempo;
784
785 /* Always reset percussion channel to program 0 */
786 this->target.blocks.push_back(MidiFile::DataBlock());
787 AddMidiData(this->target.blocks.back(), MIDIST_PROGCHG + 9, 0x00);
788
789 /* Technically should be an endless loop, but having
790 * a maximum (about 10 minutes) avoids getting stuck,
791 * in case of corrupted data. */
792 for (uint32_t tick = 0; tick < 100000; tick += 1) {
793 auto &block = this->target.blocks.emplace_back();
794 block.ticktime = tick;
795 if (!this->PlayFrame(block)) {
796 break;
797 }
798 }
799 return true;
800 }
801};
803const int MpsMachine::TEMPO_RATE = 148;
805const uint8_t MpsMachine::programvelocities[128] = {
806 100, 100, 100, 100, 100, 90, 100, 100, 100, 100, 100, 90, 100, 100, 100, 100,
807 100, 100, 85, 100, 100, 100, 100, 100, 100, 100, 100, 100, 90, 90, 110, 80,
808 100, 100, 100, 90, 70, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
809 100, 100, 90, 100, 100, 100, 100, 100, 100, 120, 100, 100, 100, 120, 100, 127,
810 100, 100, 90, 100, 100, 100, 100, 100, 100, 95, 100, 100, 100, 100, 100, 100,
811 100, 100, 100, 100, 100, 100, 100, 115, 100, 100, 100, 100, 100, 100, 100, 100,
812 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
813 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
814};
815
822bool MidiFile::LoadMpsData(const uint8_t *data, size_t length)
823{
824 _midifile_instance = this;
825
826 MpsMachine machine(data, length, *this);
827 return machine.PlayInto() && FixupMidiData(*this);
828}
829
830bool MidiFile::LoadSong(const MusicSongInfo &song)
831{
832 switch (song.filetype) {
833 case MTT_STANDARDMIDI:
834 return this->LoadFile(song.filename);
835 case MTT_MPSMIDI:
836 {
837 auto songdata = GetMusicCatEntryData(song.filename, song.cat_index);
838 if (songdata.has_value()) {
839 bool result = this->LoadMpsData(songdata->data(), songdata->size());
840 return result;
841 } else {
842 return false;
843 }
844 }
845 default:
846 NOT_REACHED();
847 }
848}
849
855{
856 std::swap(this->blocks, other.blocks);
857 std::swap(this->tempos, other.tempos);
858 this->tickdiv = other.tickdiv;
859
860 _midifile_instance = this;
861
862 other.blocks.clear();
863 other.tempos.clear();
864 other.tickdiv = 0;
865}
866
867static void WriteVariableLen(FileHandle &f, uint32_t value)
868{
869 if (value <= 0x7F) {
870 uint8_t tb = value;
871 fwrite(&tb, 1, 1, f);
872 } else if (value <= 0x3FFF) {
873 uint8_t tb[2];
874 tb[1] = value & 0x7F; value >>= 7;
875 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
876 fwrite(tb, 1, sizeof(tb), f);
877 } else if (value <= 0x1FFFFF) {
878 uint8_t tb[3];
879 tb[2] = value & 0x7F; value >>= 7;
880 tb[1] = (value & 0x7F) | 0x80; value >>= 7;
881 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
882 fwrite(tb, 1, sizeof(tb), f);
883 } else if (value <= 0x0FFFFFFF) {
884 uint8_t tb[4];
885 tb[3] = value & 0x7F; value >>= 7;
886 tb[2] = (value & 0x7F) | 0x80; value >>= 7;
887 tb[1] = (value & 0x7F) | 0x80; value >>= 7;
888 tb[0] = (value & 0x7F) | 0x80; value >>= 7;
889 fwrite(tb, 1, sizeof(tb), f);
890 }
891}
892
898bool MidiFile::WriteSMF(const std::string &filename)
899{
900 auto of = FioFOpenFile(filename, "wb", Subdirectory::NO_DIRECTORY);
901 if (!of.has_value()) return false;
902 auto &f = *of;
903
904 /* SMF header */
905 const uint8_t fileheader[] = {
906 'M', 'T', 'h', 'd', // block name
907 0x00, 0x00, 0x00, 0x06, // BE32 block length, always 6 bytes
908 0x00, 0x00, // writing format 0 (all in one track)
909 0x00, 0x01, // containing 1 track (BE16)
910 (uint8_t)(this->tickdiv >> 8), (uint8_t)this->tickdiv, // tickdiv in BE16
911 };
912 fwrite(fileheader, sizeof(fileheader), 1, f);
913
914 /* Track header */
915 const uint8_t trackheader[] = {
916 'M', 'T', 'r', 'k', // block name
917 0, 0, 0, 0, // BE32 block length, unknown at this time
918 };
919 fwrite(trackheader, sizeof(trackheader), 1, f);
920 /* Determine position to write the actual track block length at */
921 size_t tracksizepos = ftell(f) - 4;
922
923 /* Write blocks in sequence */
924 uint32_t lasttime = 0;
925 size_t nexttempoindex = 0;
926 for (size_t bi = 0; bi < this->blocks.size(); bi++) {
927 DataBlock &block = this->blocks[bi];
928 TempoChange &nexttempo = this->tempos[nexttempoindex];
929
930 uint32_t timediff = block.ticktime - lasttime;
931
932 /* Check if there is a tempo change before this block */
933 if (nexttempo.ticktime < block.ticktime) {
934 timediff = nexttempo.ticktime - lasttime;
935 }
936
937 /* Write delta time for block */
938 lasttime += timediff;
939 bool needtime = false;
940 WriteVariableLen(f, timediff);
941
942 /* Write tempo change if there is one */
943 if (nexttempo.ticktime <= block.ticktime) {
944 uint8_t tempobuf[6] = { MIDIST_SMF_META, 0x51, 0x03, 0, 0, 0 };
945 tempobuf[3] = (nexttempo.tempo & 0x00FF0000) >> 16;
946 tempobuf[4] = (nexttempo.tempo & 0x0000FF00) >> 8;
947 tempobuf[5] = (nexttempo.tempo & 0x000000FF);
948 fwrite(tempobuf, sizeof(tempobuf), 1, f);
949 nexttempoindex++;
950 needtime = true;
951 }
952 /* If a tempo change occurred between two blocks, rather than
953 * at start of this one, start over with delta time for the block. */
954 if (nexttempo.ticktime < block.ticktime) {
955 /* Start loop over at same index */
956 bi--;
957 continue;
958 }
959
960 /* Write each block data command */
961 uint8_t *dp = block.data.data();
962 while (dp < block.data.data() + block.data.size()) {
963 /* Always zero delta time inside blocks */
964 if (needtime) {
965 fputc(0, f);
966 }
967 needtime = true;
968
969 /* Check message type and write appropriate number of bytes */
970 switch (*dp & 0xF0) {
971 case MIDIST_NOTEOFF:
972 case MIDIST_NOTEON:
973 case MIDIST_POLYPRESS:
974 case MIDIST_CONTROLLER:
975 case MIDIST_PITCHBEND:
976 fwrite(dp, 1, 3, f);
977 dp += 3;
978 continue;
979 case MIDIST_PROGCHG:
980 case MIDIST_CHANPRESS:
981 fwrite(dp, 1, 2, f);
982 dp += 2;
983 continue;
984 }
985
986 /* Sysex needs to measure length and write that as well */
987 if (*dp == MIDIST_SYSEX) {
988 fwrite(dp, 1, 1, f);
989 dp++;
990 uint8_t *sysexend = dp;
991 while (*sysexend != MIDIST_ENDSYSEX) sysexend++;
992 ptrdiff_t sysexlen = sysexend - dp;
993 WriteVariableLen(f, sysexlen);
994 fwrite(dp, 1, sysexend - dp, f);
995 dp = sysexend + 1;
996 continue;
997 }
998
999 /* Fail for any other commands */
1000 return false;
1001 }
1002 }
1003
1004 /* End of track marker */
1005 static const uint8_t track_end_marker[] = { 0x00, MIDIST_SMF_META, 0x2F, 0x00 };
1006 fwrite(&track_end_marker, sizeof(track_end_marker), 1, f);
1007
1008 /* Fill out the RIFF block length */
1009 size_t trackendpos = ftell(f);
1010 fseek(f, tracksizepos, SEEK_SET);
1011 uint32_t tracksize = (uint32_t)(trackendpos - tracksizepos - 4); // blindly assume we never produce files larger than 2 GB
1012 tracksize = TO_BE32(tracksize);
1013 fwrite(&tracksize, 4, 1, f);
1014
1015 return true;
1016}
1017
1025std::string MidiFile::GetSMFFile(const MusicSongInfo &song)
1026{
1027 if (song.filetype == MTT_STANDARDMIDI) {
1028 std::string filename = FioFindFullPath(Subdirectory::BASESET_DIR, song.filename);
1029 if (!filename.empty()) return filename;
1031 if (!filename.empty()) return filename;
1032
1033 return std::string();
1034 }
1035
1036 if (song.filetype != MTT_MPSMIDI) return std::string();
1037
1038 char basename[MAX_PATH];
1039 {
1040 const char *fnstart = strrchr(song.filename.c_str(), PATHSEPCHAR);
1041 if (fnstart == nullptr) {
1042 fnstart = song.filename.c_str();
1043 } else {
1044 fnstart++;
1045 }
1046
1047 /* Remove all '.' characters from filename */
1048 char *wp = basename;
1049 for (const char *rp = fnstart; *rp != '\0'; rp++) {
1050 if (*rp != '.') *wp++ = *rp;
1051 }
1052 *wp++ = '\0';
1053 }
1054
1055 std::string tempdirname = FioGetDirectory(Searchpath::SP_AUTODOWNLOAD_DIR, Subdirectory::BASESET_DIR);
1056 tempdirname += basename;
1057 AppendPathSeparator(tempdirname);
1058 FioCreateDirectory(tempdirname);
1059
1060 std::string output_filename = tempdirname + std::to_string(song.cat_index) + ".mid";
1061
1062 if (FileExists(output_filename)) {
1063 /* If the file already exists, assume it's the correct decoded data */
1064 return output_filename;
1065 }
1066
1067 auto songdata = GetMusicCatEntryData(song.filename, song.cat_index);
1068 if (!songdata.has_value()) return std::string();
1069
1070 MidiFile midifile;
1071 if (!midifile.LoadMpsData(songdata->data(), songdata->size())) {
1072 return std::string();
1073 }
1074
1075 if (midifile.WriteSMF(output_filename)) {
1076 return output_filename;
1077 } else {
1078 return std::string();
1079 }
1080}
1081
1082
1083static bool CmdDumpSMF(uint8_t argc, char *argv[])
1084{
1085 if (argc == 0) {
1086 IConsolePrint(CC_HELP, "Write the current song to a Standard MIDI File. Usage: 'dumpsmf <filename>'.");
1087 return true;
1088 }
1089 if (argc != 2) {
1090 IConsolePrint(CC_WARNING, "You must specify a filename to write MIDI data to.");
1091 return false;
1092 }
1093
1094 if (_midifile_instance == nullptr) {
1095 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.");
1096 return false;
1097 }
1098
1099 std::string filename = fmt::format("{}{}", FiosGetScreenshotDir(), argv[1]);
1100 IConsolePrint(CC_INFO, "Dumping MIDI to '{}'.", filename);
1101
1102 if (_midifile_instance->WriteSMF(filename)) {
1103 IConsolePrint(CC_INFO, "File written successfully.");
1104 return true;
1105 } else {
1106 IConsolePrint(CC_ERROR, "An error occurred writing MIDI file.");
1107 return false;
1108 }
1109}
1110
1111static void RegisterConsoleMidiCommands()
1112{
1113 static bool registered = false;
1114 if (!registered) {
1115 IConsole::CmdRegister("dumpsmf", CmdDumpSMF);
1116 registered = true;
1117 }
1118}
1119
1120MidiFile::MidiFile()
1121{
1122 RegisterConsoleMidiCommands();
1123}
1124
1125MidiFile::~MidiFile()
1126{
1127 if (_midifile_instance == this) {
1128 _midifile_instance = nullptr;
1129 }
1130}
1131
@ MTT_MPSMIDI
MPS GM driver MIDI format (contained in a CAT file)
@ MTT_STANDARDMIDI
Standard MIDI file.
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:52
Owning byte buffer readable as a stream.
Definition midifile.cpp:62
ByteBuffer(FileHandle &file, size_t len)
Construct buffer from data in a file.
Definition midifile.cpp:73
bool IsValid() const
Return whether the buffer was constructed successfully.
Definition midifile.cpp:88
bool ReadByte(uint8_t &b)
Read a single byte from the buffer.
Definition midifile.cpp:107
bool IsEnd() const
Return whether reading has reached the end of the buffer.
Definition midifile.cpp:97
bool ReadVariableLength(uint32_t &res)
Read a MIDI file variable length value.
Definition midifile.cpp:121
bool ReadBuffer(uint8_t *dest, size_t length)
Read bytes into a buffer.
Definition midifile.cpp:139
bool Skip(size_t count)
Skip over a number of bytes in the buffer.
Definition midifile.cpp:168
bool ReadDataBlock(MidiFile::DataBlock *dest, size_t length)
Read bytes into a MidiFile::DataBlock.
Definition midifile.cpp:154
bool Rewind(size_t count)
Go a number of bytes back to re-read.
Definition midifile.cpp:181
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:89
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.
Definition fileio.cpp:144
void AppendPathSeparator(std::string &buf)
Appends, if necessary, the path separator character to the end of the string.
Definition fileio.cpp:346
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:316
bool FileExists(const std::string &filename)
Test whether the given filename exists.
Definition fileio.cpp:132
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.
Definition fileio.cpp:242
const char * FiosGetScreenshotDir()
Get the directory for screenshots.
Definition fios.cpp:602
@ 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)
int MemCmpT(const T *ptr1, const T *ptr2, size_t num=1)
Type-safe version of memcmp().
Definition mem_func.hpp:63
#define lengthof(array)
Return the length of an fixed size array.
Definition stdafx.h:280
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:160
std::vector< uint8_t > data
raw midi data contained in block
Definition midifile.hpp:23
uint32_t realtime
real-time (microseconds) since start of file this block should be triggered at
Definition midifile.hpp:22
uint32_t ticktime
tick number 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:28
uint32_t ticktime
tick number since start of file this tempo change occurs at
Definition midifile.hpp:27
std::vector< TempoChange > tempos
list of tempo changes in file
Definition midifile.hpp:33
bool LoadMpsData(const uint8_t *data, size_t length)
Create MIDI data from song data for the original Microprose music drivers.
Definition midifile.cpp:822
void MoveFrom(MidiFile &other)
Move data from other to this, and clears other.
Definition midifile.cpp:854
bool LoadFile(const std::string &filename)
Load a standard MIDI file.
Definition midifile.cpp:445
static bool ReadSMFHeader(const std::string &filename, SMFHeader &header)
Read the header of a standard MIDI file.
Definition midifile.cpp:404
std::vector< DataBlock > blocks
sequential time-annotated data of file, merged to a single track
Definition midifile.hpp:32
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:34
bool WriteSMF(const std::string &filename)
Write a Standard MIDI File containing the decoded music.
Definition midifile.cpp:898
Starting parameter and playback status for one channel/track.
Definition midifile.cpp:499
uint8_t cur_program
program selected, used for velocity scaling (lookup into programvelocities array)
Definition midifile.cpp:500
uint16_t delay
frames until next command
Definition midifile.cpp:502
uint32_t playpos
next byte to play this channel from
Definition midifile.cpp:503
uint8_t running_status
last midi status code seen
Definition midifile.cpp:501
uint32_t startpos
start position of master track
Definition midifile.cpp:504
uint32_t returnpos
next return position after playing a segment
Definition midifile.cpp:505
Decoder for "MPS MIDI" format data.
Definition midifile.cpp:497
MpsMachine(const uint8_t *data, size_t length, MidiFile &target)
Construct a TTD DOS music format decoder.
Definition midifile.cpp:547
uint16_t PlayChannelFrame(MidiFile::DataBlock &outblock, int channel)
Play one frame of data from one channel.
Definition midifile.cpp:619
const uint8_t * songdata
raw data array
Definition midifile.cpp:518
int16_t initial_tempo
starting tempo of song
Definition midifile.cpp:512
uint16_t ReadVariableLength(uint32_t &pos)
Read an SMF-style variable length value (note duration) from songdata.
Definition midifile.cpp:586
Channel channels[16]
playback status for each MIDI channel
Definition midifile.cpp:508
static const uint8_t programvelocities[128]
Base note velocities for various GM programs.
Definition midifile.cpp:805
int16_t tempo_ticks
ticker that increments when playing a frame, decrements before playing a frame
Definition midifile.cpp:510
bool PlayFrame(MidiFile::DataBlock &block)
Play one frame of data into a block.
Definition midifile.cpp:744
MidiFile & target
recipient of data
Definition midifile.cpp:520
int16_t current_tempo
threshold for actually playing a frame
Definition midifile.cpp:511
std::vector< uint32_t > segments
pointers into songdata to repeatable data segments
Definition midifile.cpp:509
size_t songdatalen
length of song data
Definition midifile.cpp:519
static const int TEMPO_RATE
Frames/ticks per second for music playback.
Definition midifile.cpp:515
void RestartSong()
Prepare for playback from the beginning.
Definition midifile.cpp:600
bool shouldplayflag
not-end-of-song flag
Definition midifile.cpp:513
bool PlayInto()
Perform playback of whole song.
Definition midifile.cpp:770
MpsMidiStatus
Overridden MIDI status codes used in the data format.
Definition midifile.cpp:523
@ MPSMIDIST_SEGMENT_RETURN
resume playing master track from stored position
Definition midifile.cpp:524
@ MPSMIDIST_ENDSONG
immediately end the song
Definition midifile.cpp:526
@ MPSMIDIST_SEGMENT_CALL
store current position of master track playback, and begin playback of a segment
Definition midifile.cpp:525
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 Stanard MIDI File.
Definition midi.h:16