OpenShot Audio Library | OpenShotAudio  0.6.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 
23 namespace juce
24 {
25 
26 namespace 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 //==============================================================================
254 MidiFile::MidiFile() : timeFormat ((short) (unsigned short) 0xe728) {}
255 
256 MidiFile::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 //==============================================================================
288 int MidiFile::getNumTracks() const noexcept
289 {
290  return tracks.size();
291 }
292 
293 const MidiMessageSequence* MidiFile::getTrack (int index) const noexcept
294 {
295  return tracks[index];
296 }
297 
298 void MidiFile::addTrack (const MidiMessageSequence& trackSequence)
299 {
300  tracks.add (new MidiMessageSequence (trackSequence));
301 }
302 
303 //==============================================================================
304 short MidiFile::getTimeFormat() const noexcept
305 {
306  return timeFormat;
307 }
308 
309 void MidiFile::setTicksPerQuarterNote (int ticks) noexcept
310 {
311  timeFormat = (short) ticks;
312 }
313 
314 void 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 //==============================================================================
346 bool 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 
405 void 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 //==============================================================================
450 bool 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 
468 bool MidiFile::writeTrack (OutputStream& mainOut, const MidiMessageSequence& ms) const
469 {
470  MemoryOutputStream out;
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 
533 struct 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 
790 static MidiFileTest midiFileTests;
791 
792 #endif
793 
794 } // namespace juce
constexpr static uint32 bigEndianInt(const void *bytes) noexcept
constexpr static 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