26namespace MidiFileHelpers
28 static void writeVariableLengthInt (OutputStream& out, uint32 v)
30 auto buffer = v & 0x7f;
32 while ((v >>= 7) != 0)
35 buffer |= ((v & 0x7f) | 0x80);
40 out.writeByte ((
char) buffer);
49 template <
typename Integral>
58 template <
typename Integral>
59 Optional<Integral> tryRead (
const uint8*& data,
size_t& remaining)
61 using Trait = ReadTrait<Integral>;
62 constexpr auto size =
sizeof (Integral);
67 const Optional<Integral> result { Trait::read (data) };
80 short numberOfTracks = 0;
83 static Optional<HeaderDetails> parseMidiHeader (
const uint8*
const initialData,
86 auto* data = initialData;
87 auto remaining = maxSize;
89 auto ch = tryRead<uint32> (data, remaining);
100 for (
int i = 0; i < 8; ++i)
102 ch = tryRead<uint32> (data, remaining);
119 const auto bytesRemaining = tryRead<uint32> (data, remaining);
121 if (! bytesRemaining.hasValue() || *bytesRemaining > remaining)
124 const auto optFileType = tryRead<uint16> (data, remaining);
126 if (! optFileType.hasValue() || 2 < *optFileType)
129 const auto optNumTracks = tryRead<uint16> (data, remaining);
131 if (! optNumTracks.hasValue() || (*optFileType == 0 && *optNumTracks != 1))
134 const auto optTimeFormat = tryRead<uint16> (data, remaining);
136 if (! optTimeFormat.hasValue())
139 HeaderDetails result;
141 result.fileType = (short) *optFileType;
142 result.timeFormat = (short) *optTimeFormat;
143 result.numberOfTracks = (short) *optNumTracks;
144 result.bytesRead = maxSize - remaining;
149 static double convertTicksToSeconds (
double time,
150 const MidiMessageSequence& tempoEvents,
154 return time / (-(timeFormat >> 8) * (timeFormat & 0xff));
156 double lastTime = 0, correctedTime = 0;
157 auto tickLen = 1.0 / (timeFormat & 0x7fff);
158 auto secsPerTick = 0.5 * tickLen;
159 auto numEvents = tempoEvents.getNumEvents();
161 for (
int i = 0; i < numEvents; ++i)
163 auto& m = tempoEvents.getEventPointer (i)->message;
164 auto eventTime = m.getTimeStamp();
166 if (eventTime >= time)
169 correctedTime += (eventTime - lastTime) * secsPerTick;
170 lastTime = eventTime;
172 if (m.isTempoMetaEvent())
173 secsPerTick = tickLen * m.getTempoSecondsPerQuarterNote();
175 while (i + 1 < numEvents)
177 auto& m2 = tempoEvents.getEventPointer (i + 1)->message;
179 if (! approximatelyEqual (m2.getTimeStamp(), eventTime))
182 if (m2.isTempoMetaEvent())
183 secsPerTick = tickLen * m2.getTempoSecondsPerQuarterNote();
189 return correctedTime + (time - lastTime) * secsPerTick;
192 template <
typename MethodType>
193 static void findAllMatchingEvents (
const OwnedArray<MidiMessageSequence>& tracks,
194 MidiMessageSequence& results,
197 for (
auto* track : tracks)
199 auto numEvents = track->getNumEvents();
201 for (
int j = 0; j < numEvents; ++j)
203 auto& m = track->getEventPointer (j)->message;
206 results.addEvent (m);
211 static MidiMessageSequence readTrack (
const uint8* data,
int size)
214 uint8 lastStatusByte = 0;
216 MidiMessageSequence result;
222 if (! delay.isValid())
225 data += delay.bytesUsed;
226 size -= delay.bytesUsed;
233 const MidiMessage mm (data, size, messSize, lastStatusByte, time);
241 result.addEvent (mm);
243 auto firstByte = *(mm.getRawData());
245 if ((firstByte & 0xf0) != 0xf0)
246 lastStatusByte = firstByte;
258 tracks.addCopiesOf (other.tracks);
264 tracks.addCopiesOf (other.tracks);
265 timeFormat = other.timeFormat;
270 : tracks (std::move (other.tracks)),
271 timeFormat (other.timeFormat)
277 tracks = std::move (other.tracks);
278 timeFormat = other.timeFormat;
290 return tracks.size();
295 return tracks[index];
339 for (
auto*
ms : tracks)
340 t = jmax (
t,
ms->getEndTime());
360 auto d =
static_cast<const uint8*
> (data.
getData());
362 const auto optHeader = MidiFileHelpers::parseMidiHeader (d, size);
368 timeFormat =
header.timeFormat;
375 const auto optChunkType = MidiFileHelpers::tryRead<uint32> (d, size);
380 const auto optChunkSize = MidiFileHelpers::tryRead<uint32> (d, size);
387 if (size < chunkSize)
400 *fileType =
header.fileType;
407 auto sequence = MidiFileHelpers::readTrack (data, size);
414 auto t1 = a->message.getTimeStamp();
415 auto t2 = b->message.getTimeStamp();
417 if (t1 < t2) return true;
418 if (t2 < t1) return false;
420 return a->message.isNoteOff() && b->message.isNoteOn();
438 for (
auto*
ms : tracks)
440 for (
int j =
ms->getNumEvents(); --
j >= 0;)
442 auto& m =
ms->getEventPointer (
j)->message;
443 m.setTimeStamp (MidiFileHelpers::convertTicksToSeconds (m.getTimeStamp(),
tempoEvents, timeFormat));
455 if (!
out.writeIntBigEndian (6))
return false;
457 if (!
out.writeShortBigEndian ((
short) tracks.size()))
return false;
458 if (!
out.writeShortBigEndian (timeFormat))
return false;
460 for (
auto*
ms : tracks)
461 if (! writeTrack (
out, *
ms))
476 for (
int i = 0; i <
ms.getNumEvents(); ++i)
478 auto&
mm =
ms.getEventPointer (i)->message;
480 if (
mm.isEndOfTrackMetaEvent())
483 auto tick = roundToInt (
mm.getTimeStamp());
485 MidiFileHelpers::writeVariableLengthInt (
out, (uint32)
delta);
488 auto* data =
mm.getRawData();
489 auto dataSize =
mm.getRawDataSize();
500 else if (statusByte == 0xf0)
507 MidiFileHelpers::writeVariableLengthInt (out, (uint32) dataSize);
510 out.
write (data, (
size_t) dataSize);
511 lastStatusByte = statusByte;
514 if (! endOfTrackEventWritten)
518 out.
write (m.getRawData(), (
size_t) m.getRawDataSize());
533struct MidiFileTest final :
public UnitTest
536 : UnitTest (
"MidiFile", UnitTestCategories::midi)
539 void runTest()
override
541 beginTest (
"ReadTrack respects running status");
543 const auto sequence = parseSequence ([] (OutputStream& os)
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 });
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());
559 beginTest (
"ReadTrack returns available messages if input is truncated");
562 const auto sequence = parseSequence ([] (OutputStream& os)
565 writeBytes (os, { 0xff });
568 expectEquals (sequence.getNumEvents(), 0);
572 const auto sequence = parseSequence ([] (OutputStream& os)
575 MidiFileHelpers::writeVariableLengthInt (os, 0xffff);
578 expectEquals (sequence.getNumEvents(), 0);
582 const auto sequence = parseSequence ([] (OutputStream& os)
585 MidiFileHelpers::writeVariableLengthInt (os, 0xffff);
586 writeBytes (os, { 0x90, 0x40 });
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);
596 beginTest (
"Header parsing works");
600 const auto header = parseHeader ([] (OutputStream&) {});
601 expect (! header.hasValue());
606 const auto header = parseHeader ([] (OutputStream& os)
608 writeBytes (os, { 0xff });
611 expect (! header.hasValue());
616 const auto header = parseHeader ([] (OutputStream& os)
618 writeBytes (os, {
'M',
'T',
'h',
'd' });
621 expect (! header.hasValue());
626 const auto header = parseHeader ([] (OutputStream& os)
628 writeBytes (os, {
'M',
'T',
'h',
'd', 0, 0, 0, 6, 0, 0, 0, 16, 0, 1 });
631 expect (! header.hasValue());
636 const auto header = parseHeader ([] (OutputStream& os)
638 writeBytes (os, {
'M',
'T',
'h',
'd', 0, 0, 0, 6, 0, 5, 0, 16, 0, 1 });
641 expect (! header.hasValue());
646 const auto header = parseHeader ([] (OutputStream& os)
648 writeBytes (os, {
'M',
'T',
'h',
'd', 0, 0, 0, 6, 0, 1, 0, 16, 0, 1 });
651 expect (header.hasValue());
653 expectEquals (header->fileType, (
short) 1);
654 expectEquals (header->numberOfTracks, (
short) 16);
655 expectEquals (header->timeFormat, (
short) 1);
656 expectEquals ((
int) header->bytesRead, 14);
660 beginTest (
"Read from stream");
664 const auto file = parseFile ([] (OutputStream&) {});
665 expect (! file.hasValue());
670 const auto file = parseFile ([] (OutputStream& os)
672 writeBytes (os, {
'M',
'T',
'h',
'd' });
675 expect (! file.hasValue());
680 const auto file = parseFile ([] (OutputStream& os)
682 writeBytes (os, {
'M',
'T',
'h',
'd', 0, 0, 0, 6, 0, 1, 0, 0, 0, 1 });
685 expect (file.hasValue());
686 expectEquals (file->getNumTracks(), 0);
691 const auto file = parseFile ([] (OutputStream& os)
693 writeBytes (os, {
'M',
'T',
'h',
'd', 0, 0, 0, 6, 0, 1, 0, 1, 0, 1 });
694 writeBytes (os, {
'M',
'T',
'r',
'?' });
697 expect (! file.hasValue());
702 const auto file = parseFile ([] (OutputStream& os)
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 });
708 expect (file.hasValue());
709 expectEquals (file->getNumTracks(), 1);
710 expectEquals (file->getTrack (0)->getNumEvents(), 0);
715 const auto file = parseFile ([] (OutputStream& os)
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 });
721 expect (! file.hasValue());
726 const auto file = parseFile ([] (OutputStream& os)
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 });
731 MidiFileHelpers::writeVariableLengthInt (os, 0x0f);
732 writeBytes (os, { 0x80, 0x00, 0x00 });
735 expect (file.hasValue());
736 expectEquals (file->getNumTracks(), 1);
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);
746 template <
typename Fn>
747 static MidiMessageSequence parseSequence (Fn&& fn)
749 MemoryOutputStream os;
752 return MidiFileHelpers::readTrack (
reinterpret_cast<const uint8*
> (os.getData()),
753 (
int) os.getDataSize());
756 template <
typename Fn>
757 static Optional<MidiFileHelpers::HeaderDetails> parseHeader (Fn&& fn)
759 MemoryOutputStream os;
762 return MidiFileHelpers::parseMidiHeader (
reinterpret_cast<const uint8*
> (os.getData()),
766 template <
typename Fn>
767 static Optional<MidiFile> parseFile (Fn&& fn)
769 MemoryOutputStream os;
772 MemoryInputStream is (os.getData(), os.getDataSize(),
false);
777 if (mf.readFrom (is,
true, &fileType))
783 static void writeBytes (OutputStream& os,
const std::vector<uint8>& bytes)
785 for (
const auto&
byte : bytes)
786 os.writeByte ((char) byte);
790static MidiFileTest midiFileTests;
static constexpr uint32 bigEndianInt(const void *bytes) noexcept
static constexpr uint16 bigEndianShort(const void *bytes) noexcept
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
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)