DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
MidiFile.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
25#include <algorithm>
26#include <cstddef>
27#include <cstdint>
28#include <cstring>
29#include <filesystem>
30#include <fstream>
31#include <limits>
32#include <optional>
33#include <span>
34#include <utility>
35#include <vector>
36
37namespace dspark {
38
40enum class MidiEventKind : uint8_t
41{
42 Channel,
43 SysExF0,
44 SysExF7,
45 Meta
46};
47
55{
56 uint32_t deltaTicks = 0;
58 uint8_t status = 0;
59 uint8_t data1 = 0;
60 uint8_t data2 = 0;
61 uint8_t metaType = 0;
62 std::vector<uint8_t> payload;
63
64 friend bool operator==(const MidiEvent&, const MidiEvent&) = default;
65};
66
69{
70 std::vector<MidiEvent> events;
71
72 friend bool operator==(const MidiTrack&, const MidiTrack&) = default;
73};
74
77{
78 uint64_t tick = 0;
79 uint32_t microsecondsPerQuarter = 500000;
80};
81
95{
96public:
98 static constexpr uint64_t kMaxInputBytes = 256ull * 1024 * 1024;
100 static constexpr uint32_t kMaxTracks = 4096;
102 static constexpr uint64_t kMaxEvents = 2000000;
104 static constexpr uint64_t kMaxAggregatePayloadBytes = 128ull * 1024 * 1024;
106 static constexpr uint64_t kMaxTrackChunkBytes = 128ull * 1024 * 1024;
107
115 [[nodiscard]] bool create(uint16_t format, uint16_t ppqn,
116 size_t trackCount = 1)
117 {
118 if ((format != 0 && format != 1) || ppqn == 0 || ppqn > 0x7fffu)
119 return false;
120 if (trackCount == 0 || trackCount > kMaxTracks)
121 return false;
122 if (format == 0 && trackCount != 1)
123 return false;
124
125 std::vector<MidiTrack> replacement(trackCount);
126 std::vector<TrackAuthoringState> replacementStates(trackCount);
127 format_ = static_cast<int>(format);
128 ppqn_ = ppqn;
129 tracks_ = std::move(replacement);
130 validationTotals_ = {};
131 trackAuthoringStates_ = std::move(replacementStates);
132 return true;
133 }
134
139 [[nodiscard]] std::optional<size_t> addTrack()
140 {
141 if (format_ != 1 || tracks_.size() >= kMaxTracks)
142 return std::nullopt;
143 trackAuthoringStates_.emplace_back();
144 try
145 {
146 tracks_.emplace_back();
147 }
148 catch (...)
149 {
150 trackAuthoringStates_.pop_back();
151 throw;
152 }
153 return tracks_.size() - 1;
154 }
155
163 [[nodiscard]] bool addChannelEvent(size_t track, uint32_t delta,
164 uint8_t status, uint8_t data1,
165 uint8_t data2 = 0)
166 {
167 MidiEvent event;
168 event.deltaTicks = delta;
169 event.kind = MidiEventKind::Channel;
170 event.status = status;
171 event.data1 = data1;
172 event.data2 = data2;
173 return appendEvent(track, std::move(event));
174 }
175
183 [[nodiscard]] bool addSysExEvent(size_t track, uint32_t delta,
184 MidiEventKind f0OrF7,
185 std::span<const uint8_t> payload)
186 {
187 if (!preflightPayloadEvent(track, delta, f0OrF7, 0, payload))
188 return false;
189 MidiEvent event;
190 event.deltaTicks = delta;
191 event.kind = f0OrF7;
192 event.payload.assign(payload.begin(), payload.end());
193 return appendEvent(track, std::move(event));
194 }
195
197 [[nodiscard]] bool addMetaEvent(size_t track, uint32_t delta, uint8_t type,
198 std::span<const uint8_t> payload = {})
199 {
200 if (!preflightPayloadEvent(track, delta, MidiEventKind::Meta,
201 type, payload))
202 return false;
203 MidiEvent event;
204 event.deltaTicks = delta;
205 event.kind = MidiEventKind::Meta;
206 event.metaType = type;
207 event.payload.assign(payload.begin(), payload.end());
208 return appendEvent(track, std::move(event));
209 }
210
212 void clear() noexcept
213 {
214 format_ = -1;
215 ppqn_ = 0;
216 tracks_.clear();
217 validationTotals_ = {};
218 trackAuthoringStates_.clear();
219 }
220
225 [[nodiscard]] bool read(const std::filesystem::path& path)
226 {
227 clear();
228
229 std::ifstream input(path, std::ios::binary | std::ios::ate);
230 if (!input.is_open())
231 return false;
232 const std::streampos end = input.tellg();
233 if (end <= std::streampos(0))
234 return false;
235 const auto fileSize = static_cast<uint64_t>(end);
236 if (fileSize > kMaxInputBytes
237 || fileSize > static_cast<uint64_t>(std::numeric_limits<size_t>::max()))
238 return false;
239
240 std::vector<uint8_t> bytes(static_cast<size_t>(fileSize));
241 input.seekg(0, std::ios::beg);
242 input.read(reinterpret_cast<char*>(bytes.data()),
243 static_cast<std::streamsize>(bytes.size()));
244 if (input.gcount() != static_cast<std::streamsize>(bytes.size()))
245 return false;
246
247 MidiFile parsed;
248 if (!parseFile(bytes, parsed))
249 return false;
250 *this = std::move(parsed);
251 return true;
252 }
253
260 [[nodiscard]] bool write(const std::filesystem::path& path) const
261 {
262 ValidationTotals totals;
263 if (format_ != 0 && format_ != 1)
264 return false;
265 if (!validateDocument(format_, ppqn_, tracks_, false, false, totals))
266 return false;
267
268 uint64_t implicitEotCount = 0;
269 for (const MidiTrack& track : tracks_)
270 if (track.events.empty() || !isEot(track.events.back()))
271 ++implicitEotCount;
272 if (implicitEotCount > kMaxEvents - totals.events)
273 return false;
274
275 std::vector<uint8_t> output;
276 output.reserve(14);
277 appendTag(output, "MThd");
278 appendBE32(output, 6);
279 appendBE16(output, static_cast<uint16_t>(format_));
280 appendBE16(output, static_cast<uint16_t>(tracks_.size()));
281 appendBE16(output, ppqn_);
282
283 for (const MidiTrack& track : tracks_)
284 {
285 std::vector<uint8_t> data;
286 for (const MidiEvent& event : track.events)
287 {
288 appendVlq(data, event.deltaTicks);
289 switch (event.kind)
290 {
292 data.push_back(event.status);
293 data.push_back(event.data1);
294 if (channelDataCount(event.status) == 2)
295 data.push_back(event.data2);
296 break;
299 data.push_back(event.kind == MidiEventKind::SysExF0
300 ? uint8_t { 0xf0 } : uint8_t { 0xf7 });
301 appendVlq(data, static_cast<uint32_t>(event.payload.size()));
302 data.insert(data.end(), event.payload.begin(), event.payload.end());
303 break;
305 data.push_back(0xff);
306 data.push_back(event.metaType);
307 appendVlq(data, static_cast<uint32_t>(event.payload.size()));
308 data.insert(data.end(), event.payload.begin(), event.payload.end());
309 break;
310 }
311 }
312
313 if (track.events.empty() || !isEot(track.events.back()))
314 {
315 data.push_back(0x00);
316 data.push_back(0xff);
317 data.push_back(0x2f);
318 data.push_back(0x00);
319 }
320
321 if (data.size() > kMaxTrackChunkBytes
322 || data.size() > std::numeric_limits<uint32_t>::max())
323 return false;
324 if (output.size() > kMaxInputBytes - 8
325 || data.size() > kMaxInputBytes - output.size() - 8)
326 return false;
327 appendTag(output, "MTrk");
328 appendBE32(output, static_cast<uint32_t>(data.size()));
329 output.insert(output.end(), data.begin(), data.end());
330 }
331
332 std::ofstream destination(path, std::ios::binary | std::ios::trunc);
333 if (!destination.is_open())
334 return false;
335 destination.write(reinterpret_cast<const char*>(output.data()),
336 static_cast<std::streamsize>(output.size()));
337 return destination.good();
338 }
339
341 [[nodiscard]] int format() const noexcept { return format_; }
342
344 [[nodiscard]] uint16_t ticksPerQuarter() const noexcept { return ppqn_; }
345
354 [[nodiscard]] const std::vector<MidiTrack>& tracks() const noexcept
355 {
356 return tracks_;
357 }
358
366 [[nodiscard]] std::optional<std::vector<MidiTempoChange>>
367 tempoMap(size_t track = 0) const
368 {
369 const std::optional<size_t> source = tempoTrack(track);
370 if (!source)
371 return std::nullopt;
372
373 std::vector<MidiTempoChange> result;
374 result.push_back({ 0, 500000 });
375 uint64_t absoluteTick = 0;
376 for (const MidiEvent& event : tracks_[*source].events)
377 {
378 if (absoluteTick > std::numeric_limits<uint64_t>::max() - event.deltaTicks)
379 return std::nullopt;
380 absoluteTick += event.deltaTicks;
381 uint32_t tempo = 0;
382 if (!getTempo(event, tempo))
383 continue;
384 if (result.back().tick == absoluteTick)
385 result.back().microsecondsPerQuarter = tempo;
386 else
387 result.push_back({ absoluteTick, tempo });
388 }
389 return result;
390 }
391
396 [[nodiscard]] std::optional<uint64_t>
397 tickToMicroseconds(uint64_t tick, size_t track = 0) const noexcept
398 {
399 uint64_t whole = 0;
400 uint64_t remainder = 0;
401 if (!integrateTicks(tick, track, whole, remainder))
402 return std::nullopt;
403 return whole;
404 }
405
410 [[nodiscard]] std::optional<double>
411 tickToSeconds(uint64_t tick, size_t track = 0) const noexcept
412 {
413 uint64_t whole = 0;
414 uint64_t remainder = 0;
415 if (!integrateTicks(tick, track, whole, remainder))
416 return std::nullopt;
417 return secondsFromRationalMicroseconds(whole, remainder, ppqn_);
418 }
419
420private:
421 struct ValidationTotals
422 {
423 uint64_t events = 0;
424 uint64_t payloadBytes = 0;
425 };
426
427 struct TrackAuthoringState
428 {
429 uint64_t absoluteTick = 0;
430 bool sysexOpen = false;
431 bool sawEot = false;
432 };
433
434 class ByteCursor
435 {
436 public:
437 ByteCursor(const std::vector<uint8_t>& bytes, size_t begin, size_t end)
438 : bytes_(bytes), pos_(begin), end_(end)
439 {
440 }
441
442 [[nodiscard]] size_t remaining() const noexcept { return end_ - pos_; }
443 [[nodiscard]] size_t position() const noexcept { return pos_; }
444
445 bool readU8(uint8_t& value) noexcept
446 {
447 if (pos_ >= end_) return false;
448 value = bytes_[pos_++];
449 return true;
450 }
451
452 bool readBE16(uint16_t& value) noexcept
453 {
454 uint8_t a = 0, b = 0;
455 if (!readU8(a) || !readU8(b)) return false;
456 value = static_cast<uint16_t>((static_cast<uint16_t>(a) << 8)
457 | static_cast<uint16_t>(b));
458 return true;
459 }
460
461 bool readBE32(uint32_t& value) noexcept
462 {
463 uint8_t a = 0, b = 0, c = 0, d = 0;
464 if (!readU8(a) || !readU8(b) || !readU8(c) || !readU8(d))
465 return false;
466 value = (static_cast<uint32_t>(a) << 24)
467 | (static_cast<uint32_t>(b) << 16)
468 | (static_cast<uint32_t>(c) << 8)
469 | static_cast<uint32_t>(d);
470 return true;
471 }
472
473 bool readTag(char tag[4]) noexcept
474 {
475 if (remaining() < 4) return false;
476 for (size_t i = 0; i < 4; ++i)
477 tag[i] = static_cast<char>(bytes_[pos_++]);
478 return true;
479 }
480
481 bool readSpan(size_t length, const uint8_t*& data) noexcept
482 {
483 if (length > remaining()) return false;
484 data = bytes_.data() + pos_;
485 pos_ += length;
486 return true;
487 }
488
489 bool skip(size_t length) noexcept
490 {
491 const uint8_t* ignored = nullptr;
492 return readSpan(length, ignored);
493 }
494
495 private:
496 const std::vector<uint8_t>& bytes_;
497 size_t pos_ = 0;
498 size_t end_ = 0;
499 };
500
501 int format_ = -1;
502 uint16_t ppqn_ = 0;
503 std::vector<MidiTrack> tracks_;
504 ValidationTotals validationTotals_;
505 std::vector<TrackAuthoringState> trackAuthoringStates_;
506
507 static bool sameTag(const char tag[4], const char (&expected)[5]) noexcept
508 {
509 return std::memcmp(tag, expected, 4) == 0;
510 }
511
512 static int channelDataCount(uint8_t status) noexcept
513 {
514 if (status < 0x80 || status > 0xef)
515 return 0;
516 const uint8_t high = status & 0xf0u;
517 return (high == 0xc0u || high == 0xd0u) ? 1 : 2;
518 }
519
520 static bool isEot(const MidiEvent& event) noexcept
521 {
522 return event.kind == MidiEventKind::Meta && event.metaType == 0x2f;
523 }
524
525 static bool payloadEndsF7(const MidiEvent& event) noexcept
526 {
527 return !event.payload.empty() && event.payload.back() == 0xf7;
528 }
529
530 static bool validTempoPayload(const MidiEvent& event) noexcept
531 {
532 if (event.kind != MidiEventKind::Meta || event.metaType != 0x51
533 || event.payload.size() != 3)
534 return true;
535 return event.payload[0] != 0 || event.payload[1] != 0
536 || event.payload[2] != 0;
537 }
538
539 static bool validateEventShape(const MidiEvent& event) noexcept
540 {
541 if (event.deltaTicks > 0x0fffffffu)
542 return false;
543 switch (event.kind)
544 {
546 {
547 const int count = channelDataCount(event.status);
548 if (count == 0 || event.data1 >= 0x80 || event.data2 >= 0x80)
549 return false;
550 if (count == 1 && event.data2 != 0)
551 return false;
552 return event.metaType == 0 && event.payload.empty();
553 }
556 return event.status == 0 && event.data1 == 0 && event.data2 == 0
557 && event.metaType == 0;
559 if (event.status != 0 || event.data1 != 0 || event.data2 != 0
560 || event.metaType >= 0x80 || !validTempoPayload(event))
561 return false;
562 return event.metaType != 0x2f || event.payload.empty();
563 }
564 return false;
565 }
566
567 static bool advanceTrackAuthoringState(
568 const MidiEvent& event, TrackAuthoringState& state) noexcept
569 {
570 if (state.sawEot
571 || state.absoluteTick
572 > std::numeric_limits<uint64_t>::max() - event.deltaTicks)
573 return false;
574 state.absoluteTick += event.deltaTicks;
575
576 switch (event.kind)
577 {
579 return !state.sysexOpen;
581 if (state.sysexOpen)
582 return false;
583 if (isEot(event))
584 state.sawEot = true;
585 return true;
587 if (state.sysexOpen)
588 return false;
589 state.sysexOpen = !payloadEndsF7(event);
590 return true;
592 if (state.sysexOpen)
593 state.sysexOpen = !payloadEndsF7(event);
594 return true;
595 }
596 return false;
597 }
598
599 static bool validateTrack(const MidiTrack& track, bool requireEot,
600 bool allowOpenSysEx,
601 ValidationTotals& totals,
602 TrackAuthoringState* authoringState = nullptr) noexcept
603 {
604 TrackAuthoringState state;
605 for (const MidiEvent& event : track.events)
606 {
607 if (!validateEventShape(event))
608 return false;
609 if (totals.events >= kMaxEvents)
610 return false;
611 ++totals.events;
612 if (event.payload.size() > kMaxAggregatePayloadBytes
613 || totals.payloadBytes > kMaxAggregatePayloadBytes - event.payload.size())
614 return false;
615 totals.payloadBytes += event.payload.size();
616 if (!advanceTrackAuthoringState(event, state))
617 return false;
618 }
619 if ((!allowOpenSysEx && state.sysexOpen)
620 || (requireEot && !state.sawEot))
621 return false;
622 if (authoringState != nullptr)
623 *authoringState = state;
624 return true;
625 }
626
627 static bool validateDocument(int format, uint16_t ppqn,
628 const std::vector<MidiTrack>& tracks,
629 bool requireEot,
630 bool allowOpenSysEx,
631 ValidationTotals& totals,
632 std::span<TrackAuthoringState> authoringStates = {}) noexcept
633 {
634 if (format < 0 || format > 2 || ppqn == 0 || ppqn > 0x7fff)
635 return false;
636 if (tracks.empty() || tracks.size() > kMaxTracks)
637 return false;
638 if (format == 0 && tracks.size() != 1)
639 return false;
640 if (!authoringStates.empty() && authoringStates.size() != tracks.size())
641 return false;
642 totals = {};
643 for (size_t index = 0; index < tracks.size(); ++index)
644 {
645 TrackAuthoringState* state = authoringStates.empty()
646 ? nullptr : &authoringStates[index];
647 if (!validateTrack(tracks[index], requireEot, allowOpenSysEx,
648 totals, state))
649 return false;
650 }
651 return true;
652 }
653
654 [[nodiscard]] bool appendEvent(size_t track, MidiEvent event)
655 {
656 if (format_ != 0 && format_ != 1)
657 return false;
658 if (track >= tracks_.size()
659 || trackAuthoringStates_.size() != tracks_.size()
660 || !validateEventShape(event))
661 return false;
662 if (validationTotals_.events >= kMaxEvents
663 || event.payload.size() > kMaxAggregatePayloadBytes
664 || validationTotals_.payloadBytes
665 > kMaxAggregatePayloadBytes - event.payload.size())
666 return false;
667
668 TrackAuthoringState nextState = trackAuthoringStates_[track];
669 if (!advanceTrackAuthoringState(event, nextState))
670 return false;
671 const size_t payloadBytes = event.payload.size();
672 tracks_[track].events.push_back(std::move(event));
673 ++validationTotals_.events;
674 validationTotals_.payloadBytes += payloadBytes;
675 trackAuthoringStates_[track] = nextState;
676 return true;
677 }
678
679 [[nodiscard]] bool preflightPayloadEvent(
680 size_t track, uint32_t delta, MidiEventKind kind, uint8_t metaType,
681 std::span<const uint8_t> payload) const noexcept
682 {
683 if ((format_ != 0 && format_ != 1) || track >= tracks_.size()
684 || delta > 0x0fffffffu || payload.size() > kMaxAggregatePayloadBytes)
685 return false;
686 if (kind != MidiEventKind::SysExF0
687 && kind != MidiEventKind::SysExF7
688 && kind != MidiEventKind::Meta)
689 return false;
690 if (kind == MidiEventKind::Meta)
691 {
692 if (metaType >= 0x80 || (metaType == 0x2f && !payload.empty()))
693 return false;
694 if (metaType == 0x51 && payload.size() == 3
695 && payload[0] == 0 && payload[1] == 0 && payload[2] == 0)
696 return false;
697 }
698
699 if (trackAuthoringStates_.size() != tracks_.size()
700 || validationTotals_.events >= kMaxEvents
701 || validationTotals_.payloadBytes
702 > kMaxAggregatePayloadBytes - payload.size())
703 return false;
704
705 const TrackAuthoringState& state = trackAuthoringStates_[track];
706 if (state.sawEot
707 || state.absoluteTick > std::numeric_limits<uint64_t>::max() - delta)
708 return false;
709 if (kind == MidiEventKind::SysExF0)
710 return !state.sysexOpen;
711 if (kind == MidiEventKind::SysExF7)
712 return true;
713 return !state.sysexOpen;
714 }
715
716 static bool readVlq(ByteCursor& cursor, uint32_t& value) noexcept
717 {
718 value = 0;
719 for (unsigned count = 0; count < 4; ++count)
720 {
721 uint8_t byte = 0;
722 if (!cursor.readU8(byte))
723 return false;
724 if (count == 0 && (byte & 0x80u) != 0 && (byte & 0x7fu) == 0)
725 return false;
726 value = static_cast<uint32_t>((value << 7) | (byte & 0x7fu));
727 if ((byte & 0x80u) == 0)
728 return true;
729 }
730 return false;
731 }
732
733 static bool copyPayload(ByteCursor& cursor, uint32_t length,
734 ValidationTotals& totals,
735 std::vector<uint8_t>& payload)
736 {
737 if (length > cursor.remaining()
738 || totals.payloadBytes > kMaxAggregatePayloadBytes - length)
739 return false;
740 const uint8_t* data = nullptr;
741 if (!cursor.readSpan(length, data))
742 return false;
743 payload.assign(data, data + length);
744 totals.payloadBytes += length;
745 return true;
746 }
747
748 static bool parseTrack(const std::vector<uint8_t>& bytes, size_t begin,
749 size_t end, ValidationTotals& totals,
750 MidiTrack& result)
751 {
752 ByteCursor cursor(bytes, begin, end);
753 uint8_t runningStatus = 0;
754 bool sysexOpen = false;
755 bool sawEot = false;
756 uint64_t absoluteTick = 0;
757
758 while (cursor.remaining() > 0)
759 {
760 if (sawEot || totals.events >= kMaxEvents)
761 return false;
762 MidiEvent event;
763 if (!readVlq(cursor, event.deltaTicks))
764 return false;
765 if (absoluteTick > std::numeric_limits<uint64_t>::max() - event.deltaTicks)
766 return false;
767 absoluteTick += event.deltaTicks;
768
769 uint8_t first = 0;
770 if (!cursor.readU8(first))
771 return false;
772 if (first < 0x80)
773 {
774 if (runningStatus == 0 || sysexOpen)
775 return false;
776 event.kind = MidiEventKind::Channel;
777 event.status = runningStatus;
778 event.data1 = first;
779 if (channelDataCount(runningStatus) == 2)
780 {
781 if (!cursor.readU8(event.data2) || event.data2 >= 0x80)
782 return false;
783 }
784 }
785 else if (first >= 0x80 && first <= 0xef)
786 {
787 if (sysexOpen)
788 return false;
789 event.kind = MidiEventKind::Channel;
790 event.status = first;
791 runningStatus = first;
792 if (!cursor.readU8(event.data1) || event.data1 >= 0x80)
793 return false;
794 if (channelDataCount(first) == 2)
795 {
796 if (!cursor.readU8(event.data2) || event.data2 >= 0x80)
797 return false;
798 }
799 }
800 else if (first == 0xf0 || first == 0xf7)
801 {
802 runningStatus = 0;
803 event.kind = first == 0xf0 ? MidiEventKind::SysExF0
805 uint32_t length = 0;
806 if (!readVlq(cursor, length)
807 || !copyPayload(cursor, length, totals, event.payload))
808 return false;
809 if (event.kind == MidiEventKind::SysExF0)
810 {
811 if (sysexOpen) return false;
812 sysexOpen = !payloadEndsF7(event);
813 }
814 else if (sysexOpen)
815 {
816 sysexOpen = !payloadEndsF7(event);
817 }
818 }
819 else if (first == 0xff)
820 {
821 runningStatus = 0;
822 if (sysexOpen)
823 return false;
824 event.kind = MidiEventKind::Meta;
825 if (!cursor.readU8(event.metaType) || event.metaType >= 0x80)
826 return false;
827 uint32_t length = 0;
828 if (!readVlq(cursor, length)
829 || !copyPayload(cursor, length, totals, event.payload)
830 || !validTempoPayload(event))
831 return false;
832 if (event.metaType == 0x2f)
833 {
834 if (!event.payload.empty()) return false;
835 sawEot = true;
836 }
837 }
838 else
839 {
840 return false;
841 }
842
843 if (!validateEventShape(event))
844 return false;
845 ++totals.events;
846 result.events.push_back(std::move(event));
847 }
848 return sawEot && !sysexOpen;
849 }
850
851 static bool parseFile(const std::vector<uint8_t>& bytes, MidiFile& result)
852 {
853 if (bytes.size() < 14 || bytes.size() > kMaxInputBytes)
854 return false;
855 ByteCursor cursor(bytes, 0, bytes.size());
856 char tag[4] = {};
857 uint32_t headerLength = 0;
858 if (!cursor.readTag(tag) || !sameTag(tag, "MThd")
859 || !cursor.readBE32(headerLength) || headerLength < 6
860 || headerLength > cursor.remaining())
861 return false;
862
863 uint16_t format = 0, trackCount = 0, division = 0;
864 if (!cursor.readBE16(format) || !cursor.readBE16(trackCount)
865 || !cursor.readBE16(division))
866 return false;
867 if (headerLength > 6 && !cursor.skip(headerLength - 6))
868 return false;
869 if (format > 2 || trackCount == 0 || trackCount > kMaxTracks
870 || (format == 0 && trackCount != 1)
871 || division == 0 || (division & 0x8000u) != 0)
872 return false;
873
874 std::vector<MidiTrack> tracks;
875 tracks.reserve(trackCount);
876 ValidationTotals totals;
877 while (cursor.remaining() > 0)
878 {
879 if (cursor.remaining() < 8)
880 return false;
881 uint32_t length = 0;
882 if (!cursor.readTag(tag) || !cursor.readBE32(length)
883 || length > cursor.remaining())
884 return false;
885 const size_t begin = cursor.position();
886 const size_t end = begin + static_cast<size_t>(length);
887
888 if (sameTag(tag, "MTrk"))
889 {
890 if (tracks.size() >= trackCount || length > kMaxTrackChunkBytes)
891 return false;
892 MidiTrack track;
893 if (!parseTrack(bytes, begin, end, totals, track))
894 return false;
895 tracks.push_back(std::move(track));
896 }
897 else
898 {
899 if (sameTag(tag, "MThd") || length > kMaxTrackChunkBytes)
900 return false;
901 }
902 if (!cursor.skip(length))
903 return false;
904 }
905 if (tracks.size() != trackCount)
906 return false;
907
908 ValidationTotals verified;
909 std::vector<TrackAuthoringState> authoringStates(tracks.size());
910 if (!validateDocument(format, division, tracks, true, false, verified,
911 authoringStates))
912 return false;
913 result.format_ = format;
914 result.ppqn_ = division;
915 result.tracks_ = std::move(tracks);
916 result.validationTotals_ = verified;
917 result.trackAuthoringStates_ = std::move(authoringStates);
918 return true;
919 }
920
921 static void appendTag(std::vector<uint8_t>& bytes, const char (&tag)[5])
922 {
923 bytes.insert(bytes.end(), tag, tag + 4);
924 }
925
926 static void appendBE16(std::vector<uint8_t>& bytes, uint16_t value)
927 {
928 bytes.push_back(static_cast<uint8_t>(value >> 8));
929 bytes.push_back(static_cast<uint8_t>(value));
930 }
931
932 static void appendBE32(std::vector<uint8_t>& bytes, uint32_t value)
933 {
934 bytes.push_back(static_cast<uint8_t>(value >> 24));
935 bytes.push_back(static_cast<uint8_t>(value >> 16));
936 bytes.push_back(static_cast<uint8_t>(value >> 8));
937 bytes.push_back(static_cast<uint8_t>(value));
938 }
939
940 static void appendVlq(std::vector<uint8_t>& bytes, uint32_t value)
941 {
942 uint8_t encoded[4] = {};
943 unsigned count = 1;
944 encoded[3] = static_cast<uint8_t>(value & 0x7fu);
945 while ((value >>= 7) != 0)
946 encoded[3 - count++] = static_cast<uint8_t>((value & 0x7fu) | 0x80u);
947 bytes.insert(bytes.end(), encoded + (4 - count), encoded + 4);
948 }
949
950 [[nodiscard]] std::optional<size_t> tempoTrack(size_t track) const noexcept
951 {
952 if (format_ < 0 || format_ > 2 || ppqn_ == 0 || track >= tracks_.size())
953 return std::nullopt;
954 return format_ == 2 ? track : size_t { 0 };
955 }
956
957 static bool getTempo(const MidiEvent& event, uint32_t& tempo) noexcept
958 {
959 if (event.kind != MidiEventKind::Meta || event.metaType != 0x51
960 || event.payload.size() != 3)
961 return false;
962 tempo = (static_cast<uint32_t>(event.payload[0]) << 16)
963 | (static_cast<uint32_t>(event.payload[1]) << 8)
964 | static_cast<uint32_t>(event.payload[2]);
965 return tempo != 0;
966 }
967
968 static bool addInterval(uint64_t ticks, uint32_t tempo, uint16_t ppqn,
969 uint64_t& whole, uint64_t& remainder) noexcept
970 {
971 const uint64_t quotient = ticks / ppqn;
972 const uint64_t tickRemainder = ticks % ppqn;
973 if (quotient != 0
974 && quotient > std::numeric_limits<uint64_t>::max() / tempo)
975 return false;
976 const uint64_t wholePart = quotient * tempo;
977 if (whole > std::numeric_limits<uint64_t>::max() - wholePart)
978 return false;
979 whole += wholePart;
980
981 const uint64_t fractional = tickRemainder * tempo + remainder;
982 const uint64_t carry = fractional / ppqn;
983 remainder = fractional % ppqn;
984 if (whole > std::numeric_limits<uint64_t>::max() - carry)
985 return false;
986 whole += carry;
987 return true;
988 }
989
990 static double secondsFromRationalMicroseconds(
991 uint64_t wholeMicroseconds, uint64_t remainder,
992 uint16_t ppqn) noexcept
993 {
994 constexpr uint64_t microsecondsPerSecond = 1000000;
995 const uint64_t wholeSeconds = wholeMicroseconds / microsecondsPerSecond;
996 const uint64_t fractionNumerator =
997 (wholeMicroseconds % microsecondsPerSecond) * ppqn + remainder;
998 const uint64_t fractionDenominator = microsecondsPerSecond * ppqn;
999 if (fractionNumerator == 0)
1000 return static_cast<double>(wholeSeconds);
1001 if (wholeSeconds == 0)
1002 return static_cast<double>(fractionNumerator)
1003 / static_cast<double>(fractionDenominator);
1004
1005 unsigned exponent = 0;
1006 for (uint64_t value = wholeSeconds; value > 1; value >>= 1)
1007 ++exponent;
1008 const unsigned fractionalBits = 52u - exponent;
1009 uint64_t units = 0;
1010 uint64_t residual = fractionNumerator;
1011 for (unsigned bit = 0; bit < fractionalBits; ++bit)
1012 {
1013 residual *= 2;
1014 units <<= 1;
1015 if (residual >= fractionDenominator)
1016 {
1017 residual -= fractionDenominator;
1018 ++units;
1019 }
1020 }
1021 const uint64_t twiceResidual = residual * 2;
1022 if (twiceResidual > fractionDenominator
1023 || (twiceResidual == fractionDenominator && (units & 1u) != 0))
1024 ++units;
1025 const uint64_t scale = uint64_t { 1 } << fractionalBits;
1026 return static_cast<double>(wholeSeconds)
1027 + static_cast<double>(units) / static_cast<double>(scale);
1028 }
1029
1030 bool integrateTicks(uint64_t targetTick, size_t track, uint64_t& whole,
1031 uint64_t& remainder) const noexcept
1032 {
1033 const std::optional<size_t> source = tempoTrack(track);
1034 if (!source)
1035 return false;
1036 whole = 0;
1037 remainder = 0;
1038 uint64_t eventTick = 0;
1039 uint64_t integratedTick = 0;
1040 uint32_t tempo = 500000;
1041
1042 for (const MidiEvent& event : tracks_[*source].events)
1043 {
1044 if (eventTick > std::numeric_limits<uint64_t>::max() - event.deltaTicks)
1045 return false;
1046 eventTick += event.deltaTicks;
1047 uint32_t nextTempo = 0;
1048 if (!getTempo(event, nextTempo))
1049 continue;
1050 if (eventTick > targetTick)
1051 break;
1052 if (!addInterval(eventTick - integratedTick, tempo, ppqn_, whole, remainder))
1053 return false;
1054 integratedTick = eventTick;
1055 tempo = nextTempo;
1056 }
1057 return addInterval(targetTick - integratedTick, tempo, ppqn_, whole, remainder);
1058 }
1059};
1060
1061} // namespace dspark
Transactional, resource-bounded SMF parser and authoring API.
Definition MidiFile.h:95
int format() const noexcept
Returns 0, 1, or 2; returns -1 for an empty object.
Definition MidiFile.h:341
uint16_t ticksPerQuarter() const noexcept
Returns PPQN, or zero for an empty object.
Definition MidiFile.h:344
bool write(const std::filesystem::path &path) const
Writes a validated format 0 or 1 document.
Definition MidiFile.h:260
static constexpr uint32_t kMaxTracks
Maximum number of track chunks.
Definition MidiFile.h:100
bool addSysExEvent(size_t track, uint32_t delta, MidiEventKind f0OrF7, std::span< const uint8_t > payload)
Adds an F0 or F7 SysEx packet event.
Definition MidiFile.h:183
std::optional< std::vector< MidiTempoChange > > tempoMap(size_t track=0) const
Builds the effective tempo map for a track.
Definition MidiFile.h:367
std::optional< size_t > addTrack()
Adds an empty track to a format 1 document.
Definition MidiFile.h:139
void clear() noexcept
Restores the empty-state sentinel.
Definition MidiFile.h:212
static constexpr uint64_t kMaxAggregatePayloadBytes
Maximum aggregate SysEx and meta payload bytes.
Definition MidiFile.h:104
static constexpr uint64_t kMaxTrackChunkBytes
Maximum bytes in one track or skipped alien chunk.
Definition MidiFile.h:106
std::optional< uint64_t > tickToMicroseconds(uint64_t tick, size_t track=0) const noexcept
Converts an absolute tick to floor(exact elapsed microseconds).
Definition MidiFile.h:397
bool read(const std::filesystem::path &path)
Reads and validates an SMF format 0, 1, or 2 file.
Definition MidiFile.h:225
std::optional< double > tickToSeconds(uint64_t tick, size_t track=0) const noexcept
Converts an absolute tick directly from the shared rational sum.
Definition MidiFile.h:411
bool addChannelEvent(size_t track, uint32_t delta, uint8_t status, uint8_t data1, uint8_t data2=0)
Adds a validated MIDI channel event.
Definition MidiFile.h:163
static constexpr uint64_t kMaxInputBytes
Maximum accepted or produced file size.
Definition MidiFile.h:98
const std::vector< MidiTrack > & tracks() const noexcept
Returns immutable semantic tracks as an owner-thread reference view.
Definition MidiFile.h:354
static constexpr uint64_t kMaxEvents
Maximum aggregate event count.
Definition MidiFile.h:102
bool create(uint16_t format, uint16_t ppqn, size_t trackCount=1)
Replaces the document with an empty writable format 0 or 1 file.
Definition MidiFile.h:115
bool addMetaEvent(size_t track, uint32_t delta, uint8_t type, std::span< const uint8_t > payload={})
Adds an opaque meta event.
Definition MidiFile.h:197
Main namespace for the DSPark framework.
MidiEventKind
Semantic kind of a Standard MIDI File event.
Definition MidiFile.h:41
One semantic SMF event with a delta time.
Definition MidiFile.h:55
uint32_t deltaTicks
Definition MidiFile.h:56
std::vector< uint8_t > payload
Definition MidiFile.h:62
uint8_t metaType
Definition MidiFile.h:61
MidiEventKind kind
Definition MidiFile.h:57
friend bool operator==(const MidiEvent &, const MidiEvent &)=default
Effective tempo at an absolute tick.
Definition MidiFile.h:77
uint32_t microsecondsPerQuarter
Definition MidiFile.h:79
Ordered events belonging to one SMF track chunk.
Definition MidiFile.h:69
std::vector< MidiEvent > events
Definition MidiFile.h:70
friend bool operator==(const MidiTrack &, const MidiTrack &)=default