OpenShot Audio Library | OpenShotAudio 0.4.0
juce_MidiFile.cpp
1/*
2 ==============================================================================
3
4 This file is part of the JUCE library.
5 Copyright (c) 2022 - Raw Material Software Limited
6
7 JUCE is an open source library subject to commercial or open-source
8 licensing.
9
10 The code included in this file is provided under the terms of the ISC license
11 http://www.isc.org/downloads/software-support-policy/isc-license. Permission
12 To use, copy, modify, and/or distribute this software for any purpose with or
13 without fee is hereby granted provided that the above copyright notice and
14 this permission notice appear in all copies.
15
16 JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
17 EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
18 DISCLAIMED.
19
20 ==============================================================================
21*/
22
23namespace juce
24{
25
26namespace MidiFileHelpers
27{
28 static void writeVariableLengthInt (OutputStream& out, uint32 v)
29 {
30 auto buffer = v & 0x7f;
31
32 while ((v >>= 7) != 0)
33 {
34 buffer <<= 8;
35 buffer |= ((v & 0x7f) | 0x80);
36 }
37
38 for (;;)
39 {
40 out.writeByte ((char) buffer);
41
42 if (buffer & 0x80)
43 buffer >>= 8;
44 else
45 break;
46 }
47 }
48
49 template <typename Integral>
50 struct ReadTrait;
51
52 template <>
53 struct ReadTrait<uint32> { static constexpr auto read = ByteOrder::bigEndianInt; };
54
55 template <>
56 struct ReadTrait<uint16> { static constexpr auto read = ByteOrder::bigEndianShort; };
57
58 template <typename Integral>
59 Optional<Integral> tryRead (const uint8*& data, size_t& remaining)
60 {
61 using Trait = ReadTrait<Integral>;
62 constexpr auto size = sizeof (Integral);
63
64 if (remaining < size)
65 return {};
66
67 const Optional<Integral> result { Trait::read (data) };
68
69 data += size;
70 remaining -= size;
71
72 return result;
73 }
74
75 struct HeaderDetails
76 {
77 size_t bytesRead = 0;
78 short timeFormat = 0;
79 short fileType = 0;
80 short numberOfTracks = 0;
81 };
82
83 static Optional<HeaderDetails> parseMidiHeader (const uint8* const initialData,
84 const size_t maxSize)
85 {
86 auto* data = initialData;
87 auto remaining = maxSize;
88
89 auto ch = tryRead<uint32> (data, remaining);
90
91 if (! ch.hasValue())
92 return {};
93
94 if (*ch != ByteOrder::bigEndianInt ("MThd"))
95 {
96 auto ok = false;
97
98 if (*ch == ByteOrder::bigEndianInt ("RIFF"))
99 {
100 for (int i = 0; i < 8; ++i)
101 {
102 ch = tryRead<uint32> (data, remaining);
103
104 if (! ch.hasValue())
105 return {};
106
107 if (*ch == ByteOrder::bigEndianInt ("MThd"))
108 {
109 ok = true;
110 break;
111 }
112 }
113 }
114
115 if (! ok)
116 return {};
117 }
118
119 const auto bytesRemaining = tryRead<uint32> (data, remaining);
120
121 if (! bytesRemaining.hasValue() || *bytesRemaining > remaining)
122 return {};
123
124 const auto optFileType = tryRead<uint16> (data, remaining);
125
126 if (! optFileType.hasValue() || 2 < *optFileType)
127 return {};
128
129 const auto optNumTracks = tryRead<uint16> (data, remaining);
130
131 if (! optNumTracks.hasValue() || (*optFileType == 0 && *optNumTracks != 1))
132 return {};
133
134 const auto optTimeFormat = tryRead<uint16> (data, remaining);
135
136 if (! optTimeFormat.hasValue())
137 return {};
138
139 HeaderDetails result;
140
141 result.fileType = (short) *optFileType;
142 result.timeFormat = (short) *optTimeFormat;
143 result.numberOfTracks = (short) *optNumTracks;
144 result.bytesRead = maxSize - remaining;
145
146 return { result };
147 }
148
149 static double convertTicksToSeconds (double time,
150 const MidiMessageSequence& tempoEvents,
151 int timeFormat)
152 {
153 if (timeFormat < 0)
154 return time / (-(timeFormat >> 8) * (timeFormat & 0xff));
155
156 double lastTime = 0, correctedTime = 0;
157 auto tickLen = 1.0 / (timeFormat & 0x7fff);
158 auto secsPerTick = 0.5 * tickLen;
159 auto numEvents = tempoEvents.getNumEvents();
160
161 for (int i = 0; i < numEvents; ++i)
162 {
163 auto& m = tempoEvents.getEventPointer (i)->message;
164 auto eventTime = m.getTimeStamp();
165
166 if (eventTime >= time)
167 break;
168
169 correctedTime += (eventTime - lastTime) * secsPerTick;
170 lastTime = eventTime;
171
172 if (m.isTempoMetaEvent())
173 secsPerTick = tickLen * m.getTempoSecondsPerQuarterNote();
174
175 while (i + 1 < numEvents)
176 {
177 auto& m2 = tempoEvents.getEventPointer (i + 1)->message;
178
179 if (! approximatelyEqual (m2.getTimeStamp(), eventTime))
180 break;
181
182 if (m2.isTempoMetaEvent())
183 secsPerTick = tickLen * m2.getTempoSecondsPerQuarterNote();
184
185 ++i;
186 }
187 }
188
189 return correctedTime + (time - lastTime) * secsPerTick;
190 }
191
192 template <typename MethodType>
193 static void findAllMatchingEvents (const OwnedArray<MidiMessageSequence>& tracks,
194 MidiMessageSequence& results,
195 MethodType method)
196 {
197 for (auto* track : tracks)
198 {
199 auto numEvents = track->getNumEvents();
200
201 for (int j = 0; j < numEvents; ++j)
202 {
203 auto& m = track->getEventPointer (j)->message;
204
205 if ((m.*method)())
206 results.addEvent (m);
207 }
208 }
209 }
210
211 static MidiMessageSequence readTrack (const uint8* data, int size)
212 {
213 double time = 0;
214 uint8 lastStatusByte = 0;
215
216 MidiMessageSequence result;
217
218 while (size > 0)
219 {
220 const auto delay = MidiMessage::readVariableLengthValue (data, (int) size);
221
222 if (! delay.isValid())
223 break;
224
225 data += delay.bytesUsed;
226 size -= delay.bytesUsed;
227 time += delay.value;
228
229 if (size <= 0)
230 break;
231
232 int messSize = 0;
233 const MidiMessage mm (data, size, messSize, lastStatusByte, time);
234
235 if (messSize <= 0)
236 break;
237
238 size -= messSize;
239 data += messSize;
240
241 result.addEvent (mm);
242
243 auto firstByte = *(mm.getRawData());
244
245 if ((firstByte & 0xf0) != 0xf0)
246 lastStatusByte = firstByte;
247 }
248
249 return result;
250 }
251}
252
253//==============================================================================
254MidiFile::MidiFile() : timeFormat ((short) (unsigned short) 0xe728) {}
255
256MidiFile::MidiFile (const MidiFile& other) : timeFormat (other.timeFormat)
257{
258 tracks.addCopiesOf (other.tracks);
259}
260
262{
263 tracks.clear();
264 tracks.addCopiesOf (other.tracks);
265 timeFormat = other.timeFormat;
266 return *this;
267}
268
270 : tracks (std::move (other.tracks)),
271 timeFormat (other.timeFormat)
272{
273}
274
276{
277 tracks = std::move (other.tracks);
278 timeFormat = other.timeFormat;
279 return *this;
280}
281
283{
284 tracks.clear();
285}
286
287//==============================================================================
288int MidiFile::getNumTracks() const noexcept
289{
290 return tracks.size();
291}
292
293const MidiMessageSequence* MidiFile::getTrack (int index) const noexcept
294{
295 return tracks[index];
296}
297
298void MidiFile::addTrack (const MidiMessageSequence& trackSequence)
299{
300 tracks.add (new MidiMessageSequence (trackSequence));
301}
302
303//==============================================================================
304short MidiFile::getTimeFormat() const noexcept
305{
306 return timeFormat;
307}
308
309void MidiFile::setTicksPerQuarterNote (int ticks) noexcept
310{
311 timeFormat = (short) ticks;
312}
313
314void MidiFile::setSmpteTimeFormat (int framesPerSecond, int subframeResolution) noexcept
315{
316 timeFormat = (short) (((-framesPerSecond) << 8) | subframeResolution);
317}
318
319//==============================================================================
321{
322 MidiFileHelpers::findAllMatchingEvents (tracks, results, &MidiMessage::isTempoMetaEvent);
323}
324
326{
327 MidiFileHelpers::findAllMatchingEvents (tracks, results, &MidiMessage::isTimeSignatureMetaEvent);
328}
329
331{
332 MidiFileHelpers::findAllMatchingEvents (tracks, results, &MidiMessage::isKeySignatureMetaEvent);
333}
334
336{
337 double t = 0.0;
338
339 for (auto* ms : tracks)
340 t = jmax (t, ms->getEndTime());
341
342 return t;
343}
344
345//==============================================================================
346bool MidiFile::readFrom (InputStream& sourceStream,
347 bool createMatchingNoteOffs,
348 int* fileType)
349{
350 clear();
351 MemoryBlock data;
352
353 const int maxSensibleMidiFileSize = 200 * 1024 * 1024;
354
355 // (put a sanity-check on the file size, as midi files are generally small)
356 if (! sourceStream.readIntoMemoryBlock (data, maxSensibleMidiFileSize))
357 return false;
358
359 auto size = data.getSize();
360 auto d = static_cast<const uint8*> (data.getData());
361
362 const auto optHeader = MidiFileHelpers::parseMidiHeader (d, size);
363
364 if (! optHeader.hasValue())
365 return false;
366
367 const auto header = *optHeader;
368 timeFormat = header.timeFormat;
369
370 d += header.bytesRead;
371 size -= (size_t) header.bytesRead;
372
373 for (int track = 0; track < header.numberOfTracks; ++track)
374 {
375 const auto optChunkType = MidiFileHelpers::tryRead<uint32> (d, size);
376
377 if (! optChunkType.hasValue())
378 return false;
379
380 const auto optChunkSize = MidiFileHelpers::tryRead<uint32> (d, size);
381
382 if (! optChunkSize.hasValue())
383 return false;
384
385 const auto chunkSize = *optChunkSize;
386
387 if (size < chunkSize)
388 return false;
389
390 if (*optChunkType == ByteOrder::bigEndianInt ("MTrk"))
391 readNextTrack (d, (int) chunkSize, createMatchingNoteOffs);
392
393 size -= chunkSize;
394 d += chunkSize;
395 }
396
397 const auto successful = (size == 0);
398
399 if (successful && fileType != nullptr)
400 *fileType = header.fileType;
401
402 return successful;
403}
404
405void MidiFile::readNextTrack (const uint8* data, int size, bool createMatchingNoteOffs)
406{
407 auto sequence = MidiFileHelpers::readTrack (data, size);
408
409 // sort so that we put all the note-offs before note-ons that have the same time
410 std::stable_sort (sequence.list.begin(), sequence.list.end(),
413 {
414 auto t1 = a->message.getTimeStamp();
415 auto t2 = b->message.getTimeStamp();
416
417 if (t1 < t2) return true;
418 if (t2 < t1) return false;
419
420 return a->message.isNoteOff() && b->message.isNoteOn();
421 });
422
423 if (createMatchingNoteOffs)
424 sequence.updateMatchedPairs();
425
426 addTrack (sequence);
427}
428
429//==============================================================================
431{
432 MidiMessageSequence tempoEvents;
433 findAllTempoEvents (tempoEvents);
434 findAllTimeSigEvents (tempoEvents);
435
436 if (timeFormat != 0)
437 {
438 for (auto* ms : tracks)
439 {
440 for (int j = ms->getNumEvents(); --j >= 0;)
441 {
442 auto& m = ms->getEventPointer (j)->message;
443 m.setTimeStamp (MidiFileHelpers::convertTicksToSeconds (m.getTimeStamp(), tempoEvents, timeFormat));
444 }
445 }
446 }
447}
448
449//==============================================================================
450bool MidiFile::writeTo (OutputStream& out, int midiFileType) const
451{
452 jassert (midiFileType >= 0 && midiFileType <= 2);
453
454 if (! out.writeIntBigEndian ((int) ByteOrder::bigEndianInt ("MThd"))) return false;
455 if (! out.writeIntBigEndian (6)) return false;
456 if (! out.writeShortBigEndian ((short) midiFileType)) return false;
457 if (! out.writeShortBigEndian ((short) tracks.size())) return false;
458 if (! out.writeShortBigEndian (timeFormat)) return false;
459
460 for (auto* ms : tracks)
461 if (! writeTrack (out, *ms))
462 return false;
463
464 out.flush();
465 return true;
466}
467
468bool MidiFile::writeTrack (OutputStream& mainOut, const MidiMessageSequence& ms) const
469{
471
472 int lastTick = 0;
473 uint8 lastStatusByte = 0;
474 bool endOfTrackEventWritten = false;
475
476 for (int i = 0; i < ms.getNumEvents(); ++i)
477 {
478 auto& mm = ms.getEventPointer (i)->message;
479
480 if (mm.isEndOfTrackMetaEvent())
481 endOfTrackEventWritten = true;
482
483 auto tick = roundToInt (mm.getTimeStamp());
484 auto delta = jmax (0, tick - lastTick);
485 MidiFileHelpers::writeVariableLengthInt (out, (uint32) delta);
486 lastTick = tick;
487
488 auto* data = mm.getRawData();
489 auto dataSize = mm.getRawDataSize();
490 auto statusByte = data[0];
491
492 if (statusByte == lastStatusByte
493 && (statusByte & 0xf0) != 0xf0
494 && dataSize > 1
495 && i > 0)
496 {
497 ++data;
498 --dataSize;
499 }
500 else if (statusByte == 0xf0) // Write sysex message with length bytes.
501 {
502 out.writeByte ((char) statusByte);
503
504 ++data;
505 --dataSize;
506
507 MidiFileHelpers::writeVariableLengthInt (out, (uint32) dataSize);
508 }
509
510 out.write (data, (size_t) dataSize);
511 lastStatusByte = statusByte;
512 }
513
514 if (! endOfTrackEventWritten)
515 {
516 out.writeByte (0); // (tick delta)
517 auto m = MidiMessage::endOfTrack();
518 out.write (m.getRawData(), (size_t) m.getRawDataSize());
519 }
520
521 if (! mainOut.writeIntBigEndian ((int) ByteOrder::bigEndianInt ("MTrk"))) return false;
522 if (! mainOut.writeIntBigEndian ((int) out.getDataSize())) return false;
523
524 mainOut << out;
525
526 return true;
527}
528
529//==============================================================================
530//==============================================================================
531#if JUCE_UNIT_TESTS
532
533struct MidiFileTest final : public UnitTest
534{
535 MidiFileTest()
536 : UnitTest ("MidiFile", UnitTestCategories::midi)
537 {}
538
539 void runTest() override
540 {
541 beginTest ("ReadTrack respects running status");
542 {
543 const auto sequence = parseSequence ([] (OutputStream& os)
544 {
545 MidiFileHelpers::writeVariableLengthInt (os, 100);
546 writeBytes (os, { 0x90, 0x40, 0x40 });
547 MidiFileHelpers::writeVariableLengthInt (os, 200);
548 writeBytes (os, { 0x40, 0x40 });
549 MidiFileHelpers::writeVariableLengthInt (os, 300);
550 writeBytes (os, { 0xff, 0x2f, 0x00 });
551 });
552
553 expectEquals (sequence.getNumEvents(), 3);
554 expect (sequence.getEventPointer (0)->message.isNoteOn());
555 expect (sequence.getEventPointer (1)->message.isNoteOn());
556 expect (sequence.getEventPointer (2)->message.isEndOfTrackMetaEvent());
557 }
558
559 beginTest ("ReadTrack returns available messages if input is truncated");
560 {
561 {
562 const auto sequence = parseSequence ([] (OutputStream& os)
563 {
564 // Incomplete delta time
565 writeBytes (os, { 0xff });
566 });
567
568 expectEquals (sequence.getNumEvents(), 0);
569 }
570
571 {
572 const auto sequence = parseSequence ([] (OutputStream& os)
573 {
574 // Complete delta with no following event
575 MidiFileHelpers::writeVariableLengthInt (os, 0xffff);
576 });
577
578 expectEquals (sequence.getNumEvents(), 0);
579 }
580
581 {
582 const auto sequence = parseSequence ([] (OutputStream& os)
583 {
584 // Complete delta with malformed following event
585 MidiFileHelpers::writeVariableLengthInt (os, 0xffff);
586 writeBytes (os, { 0x90, 0x40 });
587 });
588
589 expectEquals (sequence.getNumEvents(), 1);
590 expect (sequence.getEventPointer (0)->message.isNoteOff());
591 expectEquals (sequence.getEventPointer (0)->message.getNoteNumber(), 0x40);
592 expectEquals (sequence.getEventPointer (0)->message.getVelocity(), (uint8) 0x00);
593 }
594 }
595
596 beginTest ("Header parsing works");
597 {
598 {
599 // No data
600 const auto header = parseHeader ([] (OutputStream&) {});
601 expect (! header.hasValue());
602 }
603
604 {
605 // Invalid initial byte
606 const auto header = parseHeader ([] (OutputStream& os)
607 {
608 writeBytes (os, { 0xff });
609 });
610
611 expect (! header.hasValue());
612 }
613
614 {
615 // Type block, but no header data
616 const auto header = parseHeader ([] (OutputStream& os)
617 {
618 writeBytes (os, { 'M', 'T', 'h', 'd' });
619 });
620
621 expect (! header.hasValue());
622 }
623
624 {
625 // We (ll-formed header, but track type is 0 and channels != 1
626 const auto header = parseHeader ([] (OutputStream& os)
627 {
628 writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 0, 0, 16, 0, 1 });
629 });
630
631 expect (! header.hasValue());
632 }
633
634 {
635 // Well-formed header, but track type is 5
636 const auto header = parseHeader ([] (OutputStream& os)
637 {
638 writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 5, 0, 16, 0, 1 });
639 });
640
641 expect (! header.hasValue());
642 }
643
644 {
645 // Well-formed header
646 const auto header = parseHeader ([] (OutputStream& os)
647 {
648 writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 16, 0, 1 });
649 });
650
651 expect (header.hasValue());
652
653 expectEquals (header->fileType, (short) 1);
654 expectEquals (header->numberOfTracks, (short) 16);
655 expectEquals (header->timeFormat, (short) 1);
656 expectEquals ((int) header->bytesRead, 14);
657 }
658 }
659
660 beginTest ("Read from stream");
661 {
662 {
663 // Empty input
664 const auto file = parseFile ([] (OutputStream&) {});
665 expect (! file.hasValue());
666 }
667
668 {
669 // Malformed header
670 const auto file = parseFile ([] (OutputStream& os)
671 {
672 writeBytes (os, { 'M', 'T', 'h', 'd' });
673 });
674
675 expect (! file.hasValue());
676 }
677
678 {
679 // Header, no channels
680 const auto file = parseFile ([] (OutputStream& os)
681 {
682 writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 0, 0, 1 });
683 });
684
685 expect (file.hasValue());
686 expectEquals (file->getNumTracks(), 0);
687 }
688
689 {
690 // Header, one malformed channel
691 const auto file = parseFile ([] (OutputStream& os)
692 {
693 writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 });
694 writeBytes (os, { 'M', 'T', 'r', '?' });
695 });
696
697 expect (! file.hasValue());
698 }
699
700 {
701 // Header, one channel with malformed message
702 const auto file = parseFile ([] (OutputStream& os)
703 {
704 writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 });
705 writeBytes (os, { 'M', 'T', 'r', 'k', 0, 0, 0, 1, 0xff });
706 });
707
708 expect (file.hasValue());
709 expectEquals (file->getNumTracks(), 1);
710 expectEquals (file->getTrack (0)->getNumEvents(), 0);
711 }
712
713 {
714 // Header, one channel with incorrect length message
715 const auto file = parseFile ([] (OutputStream& os)
716 {
717 writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 });
718 writeBytes (os, { 'M', 'T', 'r', 'k', 0x0f, 0, 0, 0, 0xff });
719 });
720
721 expect (! file.hasValue());
722 }
723
724 {
725 // Header, one channel, all well-formed
726 const auto file = parseFile ([] (OutputStream& os)
727 {
728 writeBytes (os, { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 });
729 writeBytes (os, { 'M', 'T', 'r', 'k', 0, 0, 0, 4 });
730
731 MidiFileHelpers::writeVariableLengthInt (os, 0x0f);
732 writeBytes (os, { 0x80, 0x00, 0x00 });
733 });
734
735 expect (file.hasValue());
736 expectEquals (file->getNumTracks(), 1);
737
738 auto& track = *file->getTrack (0);
739 expectEquals (track.getNumEvents(), 1);
740 expect (track.getEventPointer (0)->message.isNoteOff());
741 expectEquals (track.getEventPointer (0)->message.getTimeStamp(), (double) 0x0f);
742 }
743 }
744 }
745
746 template <typename Fn>
747 static MidiMessageSequence parseSequence (Fn&& fn)
748 {
749 MemoryOutputStream os;
750 fn (os);
751
752 return MidiFileHelpers::readTrack (reinterpret_cast<const uint8*> (os.getData()),
753 (int) os.getDataSize());
754 }
755
756 template <typename Fn>
757 static Optional<MidiFileHelpers::HeaderDetails> parseHeader (Fn&& fn)
758 {
759 MemoryOutputStream os;
760 fn (os);
761
762 return MidiFileHelpers::parseMidiHeader (reinterpret_cast<const uint8*> (os.getData()),
763 os.getDataSize());
764 }
765
766 template <typename Fn>
767 static Optional<MidiFile> parseFile (Fn&& fn)
768 {
769 MemoryOutputStream os;
770 fn (os);
771
772 MemoryInputStream is (os.getData(), os.getDataSize(), false);
773 MidiFile mf;
774
775 int fileType = 0;
776
777 if (mf.readFrom (is, true, &fileType))
778 return mf;
779
780 return {};
781 }
782
783 static void writeBytes (OutputStream& os, const std::vector<uint8>& bytes)
784 {
785 for (const auto& byte : bytes)
786 os.writeByte ((char) byte);
787 }
788};
789
790static MidiFileTest midiFileTests;
791
792#endif
793
794} // namespace juce
static constexpr uint32 bigEndianInt(const void *bytes) noexcept
static constexpr uint16 bigEndianShort(const void *bytes) noexcept
virtual size_t readIntoMemoryBlock(MemoryBlock &destBlock, ssize_t maxNumBytesToRead=-1)
void * getData() noexcept
size_t getSize() const noexcept
size_t getDataSize() const noexcept
bool write(const void *, size_t) override
void convertTimestampTicksToSeconds()
void addTrack(const MidiMessageSequence &trackSequence)
void setTicksPerQuarterNote(int ticksPerQuarterNote) noexcept
int getNumTracks() const noexcept
short getTimeFormat() const noexcept
void setSmpteTimeFormat(int framesPerSecond, int subframeResolution) noexcept
double getLastTimestamp() const
bool readFrom(InputStream &sourceStream, bool createMatchingNoteOffs=true, int *midiFileType=nullptr)
MidiFile & operator=(const MidiFile &)
void findAllTimeSigEvents(MidiMessageSequence &timeSigEvents) const
void findAllKeySigEvents(MidiMessageSequence &keySigEvents) const
void findAllTempoEvents(MidiMessageSequence &tempoChangeEvents) const
const MidiMessageSequence * getTrack(int index) const noexcept
bool writeTo(OutputStream &destStream, int midiFileType=1) const
MidiEventHolder * getEventPointer(int index) const noexcept
bool isKeySignatureMetaEvent() const noexcept
bool isTimeSignatureMetaEvent() const noexcept
bool isTempoMetaEvent() const noexcept
static MidiMessage endOfTrack() noexcept
static VariableLengthValue readVariableLengthValue(const uint8 *data, int maxBytesToUse) noexcept
virtual bool writeByte(char byte)
virtual bool writeIntBigEndian(int value)
virtual bool writeShortBigEndian(short value)
virtual void flush()=0