1 /**
2 	This file is a port of some old C code I had for reading and writing .mid files. Not much docs, but viewing the source may be helpful.
3 
4 	I'll eventually refactor it into something more D-like
5 
6 	History:
7 		Written in C in August 2008
8 
9 		Minimally ported to D in September 2017
10 
11 		Updated May 2020 with significant changes.
12 */
13 module arsd.midi;
14 
15 
16 /+
17 	So the midi ticks are defined in terms of per quarter note so that's good stuff.
18 
19 	If you're reading live though you have milliseconds, and probably want to round them
20 	off a little to fit the beat.
21 +/
22 
23 import core.time;
24 
25 version(NewMidiDemo)
26 void main(string[] args) {
27 	auto f = new MidiFile();
28 
29 	import std.file;
30 
31 	//f.loadFromBytes(cast(ubyte[]) read("test.mid"));
32 	f.loadFromBytes(cast(ubyte[]) read(args[1]));
33 
34 	import arsd.simpleaudio;
35 	import core.thread;
36 
37 	auto o = MidiOutput(0);
38 	setSigIntHandler();
39 	scope(exit) {
40 		o.silenceAllNotes();
41 		o.reset();
42 		restoreSigIntHandler();
43 	}
44 
45 	import std.stdio : writeln;
46 	foreach(item; f.playbackStream) {
47 		if(interrupted) return;
48 
49 		Thread.sleep(item.wait);
50 		if(!item.event.isMeta)
51 			o.writeMidiMessage(item.event.status, item.event.data1, item.event.data2);
52 		else
53 			writeln(item);
54 	}
55 
56 	return;
57 
58 	auto t = new MidiTrack();
59 	auto t2 = new MidiTrack();
60 
61 	f.tracks ~= t;
62 	f.tracks ~= t2;
63 
64 	t.events ~= MidiEvent(0, 0x90, C, 127);
65 	t.events ~= MidiEvent(256, 0x90, C, 0);
66 	t.events ~= MidiEvent(256, 0x90, D, 127);
67 	t.events ~= MidiEvent(256, 0x90, D, 0);
68 	t.events ~= MidiEvent(256, 0x90, E, 127);
69 	t.events ~= MidiEvent(256, 0x90, E, 0);
70 	t.events ~= MidiEvent(256, 0x90, F, 127);
71 	t.events ~= MidiEvent(0, 0xff, 0x05, 0 /* unused */, ['h', 'a', 'm']);
72 	t.events ~= MidiEvent(256, 0x90, F, 0);
73 
74 	t2.events ~= MidiEvent(0, (MIDI_EVENT_PROGRAM_CHANGE << 4) | 0x01, 68);
75 	t2.events ~= MidiEvent(128, 0x91, E, 127);
76 	t2.events ~= MidiEvent(0, 0xff, 0x05, 0 /* unused */, ['a', 'd', 'r']);
77 	t2.events ~= MidiEvent(1024, 0x91, E, 0);
78 
79 	write("test.mid", f.toBytes());
80 }
81 
82 @safe:
83 
84 class MidiFile {
85 	///
86 	ubyte[] toBytes() {
87 		MidiWriteBuffer buf;
88 
89 		buf.write("MThd");
90 		buf.write4(6);
91 
92 		buf.write2(format);
93 		buf.write2(cast(ushort) tracks.length);
94 		buf.write2(timing);
95 
96 		foreach(track; tracks) {
97 			auto data = track.toBytes();
98 			buf.write("MTrk");
99 			buf.write4(cast(int) data.length);
100 			buf.write(data);
101 		}
102 
103 		return buf.bytes;
104 	}
105 
106 	///
107 	void loadFromBytes(ubyte[] bytes) {
108 		// FIXME: actually read the riff header to skip properly
109 		if(bytes.length && bytes[0] == 'R')
110 			bytes = bytes[0x14 .. $];
111 
112 		MidiReadBuffer buf = MidiReadBuffer(bytes);
113 		if(buf.readChars(4) != "MThd")
114 			throw new Exception("not midi");
115 		if(buf.read4() != 6)
116 			throw new Exception("idk what this even is");
117 		this.format = buf.read2();
118 		this.tracks = new MidiTrack[](buf.read2());
119 		this.timing = buf.read2();
120 
121 		foreach(ref track; tracks) {
122 			track = new MidiTrack();
123 			track.loadFromBuffer(buf);
124 		}
125 	}
126 
127 	// when I read, I plan to cut the end of track marker off.
128 
129 	// 0 == combined into one track
130 	// 1 == multiple tracks
131 	// 2 == multiple one-track patterns
132 	ushort format = 1;
133 
134 	// FIXME
135 	ushort timing = 0x80; // 128 ticks per quarter note
136 
137 	MidiTrack[] tracks;
138 
139 	/++
140 		Returns a forward range for playback. Each item is a command, which
141 		is like the midi event but with some more annotations and control methods.
142 
143 		Modifying this MidiFile object or any of its children during playback
144 		may cause trouble.
145 
146 		Note that you do not need to handle any meta events, it keeps the
147 		tempo internally, but you can look at it if you like.
148 	+/
149 	const(PlayStreamEvent)[] playbackStream() {
150 		PlayStreamEvent[] stream;
151 		size_t size;
152 		foreach(track; tracks)
153 			size += track.events.length;
154 		stream.reserve(size);
155 
156 		Duration position;
157 
158 		static struct NoteOnInfo {
159 			PlayStreamEvent* event;
160 			int turnedOnTicks;
161 			Duration turnedOnPosition;
162 		}
163 		NoteOnInfo[] noteOnInfo = new NoteOnInfo[](128 * 16);
164 		scope(exit) noteOnInfo = null;
165 
166 		static struct LastNoteInfo {
167 			PlayStreamEvent*[6] event; // in case there's a chord
168 			int eventCount;
169 			int turnedOnTicks;
170 		}
171 		LastNoteInfo[/*16*/] lastNoteInfo = new LastNoteInfo[](16); // it doesn't allow the static array cuz of @safe and i don't wanna deal with that so just doing this, nbd alloc anyway
172 
173 		void recordOff(scope NoteOnInfo* noi, int midiClockPosition) {
174 			noi.event.noteOnDuration = position - noi.turnedOnPosition;
175 
176 			noi.event = null;
177 		}
178 
179 		// FIXME: what about rests?
180 		foreach(item; flattenedTrackStream) {
181 			position += item.wait;
182 
183 			stream ~= item;
184 
185 			if(item.event.event == MIDI_EVENT_NOTE_ON) {
186 				if(item.event.data2 == 0)
187 					goto off;
188 
189 				auto ptr = &stream[$-1];
190 
191 				auto noi = &noteOnInfo[(item.event.channel & 0x0f) * 128 + (item.event.data1 & 0x7f)];
192 
193 				if(noi.event) {
194 					recordOff(noi, item.midiClockPosition);
195 				}
196 
197 				noi.event = ptr;
198 				noi.turnedOnTicks = item.midiClockPosition;
199 				noi.turnedOnPosition = position;
200 
201 				auto lni = &lastNoteInfo[(item.event.channel & 0x0f)];
202 				if(lni.eventCount) {
203 					if(item.midiClockPosition == lni.turnedOnTicks) {
204 						if(lni.eventCount == lni.event.length)
205 							goto maxedOut;
206 						lni.event[lni.eventCount++] = ptr;
207 					} else {
208 						maxedOut:
209 						foreach(ref e; lni.event[0 .. lni.eventCount])
210 							e.midiTicksToNextNoteOnChannel = item.midiClockPosition - lni.turnedOnTicks;
211 
212 						goto frist;
213 					}
214 				} else {
215 					frist:
216 					lni.event[0] = ptr;
217 					lni.eventCount = 1;
218 					lni.turnedOnTicks = item.midiClockPosition;
219 				}
220 
221 			} else if(item.event.event == MIDI_EVENT_NOTE_OFF) {
222 				off:
223 				auto noi = &noteOnInfo[(item.event.channel & 0x0f) * 128 + (item.event.data1 & 0x7f)];
224 
225 				if(noi.event) {
226 					recordOff(noi, item.midiClockPosition);
227 				}
228 			}
229 		}
230 
231 		return stream;
232 	}
233 
234 	/++
235 		Returns a forward range for playback or analysis that flattens the midi
236 		tracks into a single stream. Each item is a command, which
237 		is like the midi event but with some more annotations and control methods.
238 
239 		Modifying this MidiFile object or any of its children during iteration
240 		may cause trouble.
241 
242 		Note that you do not need to handle any meta events, it keeps the
243 		tempo internally, but you can look at it if you like.
244 	+/
245 	FlattenedTrackStream flattenedTrackStream() {
246 		return FlattenedTrackStream(this);
247 	}
248 }
249 
250 static struct PlayStreamEvent {
251 	/// This is how long you wait until triggering this event.
252 	/// Note it may be zero.
253 	Duration wait;
254 
255 	/// And this is the midi event message.
256 	MidiEvent event;
257 
258 	string toString() const {
259 		return event.toString();
260 	}
261 
262 	/// informational. May be null if the stream didn't come from a file or tracks.
263 	MidiFile file;
264 	/// ditto
265 	MidiTrack track;
266 
267 	/++
268 		Gives the position ot the global midi clock for this event. The `event.deltaTime`
269 		is in units of the midi clock, but the actual event has the clock per-track whereas
270 		this value is global, meaning it might not be the sum of event.deltaTime to this point.
271 		(It should add up if you only sum ones with the same [track] though.
272 
273 		The midi clock is used in conjunction with the [MidiFile.timing] and current tempo
274 		state to determine a real time wait value, which you can find in the [wait] member.
275 
276 		This position is probably less useful than the running sum of [wait]s, but is provided
277 		just in case it is useful to you.
278 	+/
279 	int midiClockPosition;
280 
281 	/++
282 		The duration between this non-zero velocity note on and its associated note off.
283 
284 		Will be zero if this isn't actually a note on, the input stream was not seekable (e.g.
285 		a real time recording), or if a note off was not found ahead in the stream.
286 
287 		It is basically how long the pianist held down the key.
288 
289 		Be aware that that the note on to note off is not necessarily associated with the
290 		note you'd see on sheet music. It is more about the time the sound actually rings,
291 		but it may not exactly be that either due to the time it takes for the note to
292 		fade out.
293 	+/
294 	Duration noteOnDuration;
295 	/++
296 		This is the count of midi clock ticks after this non-zero velocity note on event (if
297 		it is not one of those, this value will be zero) and the next note that will be sounded
298 		on its same channel.
299 
300 		While rests may throw this off, this number is the most help in this struct for determining
301 		the note length you'd put on sheet music. Divide it by [MidiFile.timing] to get the number
302 		of midi quarter notes, which is directly correlated to the musical beat.
303 
304 		Will be zero if this isn't actually a note on, the input stream was not seekable (e.g.
305 		a real time recording where the next note hasn't been struck yet), or if a note off was
306 		not found ahead in the stream.
307 	+/
308 	int midiTicksToNextNoteOnChannel;
309 
310 	// when recording and working in milliseconds we prolly want to round off to the nearest 64th note, or even less fine grained at user command todeal with bad musicians (i.e. me) being off beat
311 }
312 
313 static immutable(PlayStreamEvent)[] longWait = [{wait: 1.weeks, event: {status: 0xff, data1: 0x01, meta: null}}];
314 
315 struct FlattenedTrackStream {
316 
317 	FlattenedTrackStream save() {
318 		auto copy = this;
319 		copy.trackPositions = this.trackPositions.dup;
320 		return copy;
321 	}
322 
323 	MidiFile file;
324 	this(MidiFile file) {
325 		this.file = file;
326 		this.trackPositions.length = file.tracks.length;
327 		foreach(idx, ref tp; this.trackPositions) {
328 			tp.remaining = file.tracks[idx].events[];
329 
330 			{
331 				bool copyPerformed = false;
332 
333 				// some midis do weird things
334 				// see: https://github.com/adamdruppe/arsd/issues/508
335 				// to correct:
336 				// first need to segment by the deltaTime - a non-zero, then all zeros that follow it.
337 				// then inside the same timestamp segments, put the note off (or note on with data2 == 0) first in the stream
338 				// make sure the first item has the non-zero deltaTime for the segment, then all others have 0.
339 
340 				// returns true if you need to copy then try again
341 				bool sortSegment(MidiEvent[] events) {
342 					if(events.length <= 1)
343 						return false;
344 
345 					bool hasNoteOn = false;
346 					bool needsChange = false;
347 					foreach(event; events) {
348 						if(hasNoteOn) {
349 							if(event.isNoteOff) {
350 								needsChange = true;
351 								break;
352 							}
353 						} else if(event.isNoteOn) {
354 							hasNoteOn = true;
355 						}
356 					}
357 
358 					if(!needsChange)
359 						return false;
360 
361 					if(!copyPerformed) {
362 						// so we don't modify the original file unnecessarily...
363 						return true;
364 					}
365 
366 					auto dt = events[0].deltaTime;
367 
368 					MidiEvent[8] staticBuffer;
369 					MidiEvent[] buffer;
370 					if(events.length < staticBuffer.length)
371 						buffer = staticBuffer[0 .. events.length];
372 					else
373 						buffer = new MidiEvent[](events.length);
374 
375 					size_t bufferPos;
376 
377 					// first pass, keep the note offs
378 					foreach(event; events) {
379 						if(event.isNoteOff)
380 							buffer[bufferPos++] = event;
381 					}
382 
383 					// second pass, keep the rest
384 					foreach(event; events) {
385 						if(!event.isNoteOff)
386 							buffer[bufferPos++] = event;
387 					}
388 
389 					assert(bufferPos == events.length);
390 					events[] = buffer[];
391 
392 					foreach(ref e; events)
393 						e.deltaTime = 0;
394 					events[0].deltaTime = dt;
395 
396 					return false;
397 				}
398 
399 				size_t first = 0;
400 				foreach(sortIndex, f; tp.remaining) {
401 					if(f.deltaTime != 0) {
402 						if(sortSegment(tp.remaining[first .. sortIndex])) {
403 							//  if it returns true, it needs to modify the array
404 							// but it doesn't change the iteration result, just we need to send it the copy after making it
405 							tp.remaining = tp.remaining.dup;
406 							copyPerformed = true;
407 							sortSegment(tp.remaining[first .. sortIndex]);
408 						}
409 						first = sortIndex;
410 					}
411 				}
412 
413 				if(sortSegment(tp.remaining[first .. $])) {
414 					tp.remaining = tp.remaining.dup;
415 					copyPerformed = true;
416 					sortSegment(tp.remaining[first .. $]);
417 				}
418 			}
419 
420 			tp.track = file.tracks[idx];
421 		}
422 
423 		this.currentTrack = -1;
424 		this.tempo = 500000; // microseconds per quarter note
425 		popFront();
426 	}
427 
428 	//@nogc:
429 
430 	int midiClock;
431 
432 	void popFront() {
433 		done = true;
434 		for(auto c = currentTrack + 1; c < trackPositions.length; c++) {
435 			auto tp = trackPositions[c];
436 
437 			if(tp.remaining.length && tp.remaining[0].deltaTime == tp.clock) {
438 				auto f = tp.remaining[0];
439 				trackPositions[c].remaining = tp.remaining[1 .. $];
440 				trackPositions[c].clock = 0;
441 				if(tp.remaining.length == 0 || tp.remaining[0].deltaTime > 0) {
442 					currentTrack += 1;
443 				}
444 
445 				// import arsd.core; debug writeln(c, " ", f);
446 
447 				pending = PlayStreamEvent(0.seconds, f, file, tp.track, midiClock);
448 				processPending();
449 				done = false;
450 				return;
451 			}
452 		}
453 
454 		// if nothing happened there, time to advance the clock
455 		int minWait = int.max;
456 		int minWaitTrack = -1;
457 		foreach(idx, track; trackPositions) {
458 			if(track.remaining.length) {
459 				auto dt = track.remaining[0].deltaTime - track.clock;
460 				if(dt < minWait) {
461 					minWait = dt;
462 					minWaitTrack = cast(int) idx;
463 				}
464 			}
465 		}
466 
467 		if(minWaitTrack == -1) {
468 			done = true;
469 			return;
470 		}
471 
472 		foreach(ref tp; trackPositions) {
473 			tp.clock += minWait;
474 		}
475 
476 		done = false;
477 
478 		// file.timing, if high bit clear, is ticks per quarter note
479 		// if high bit set... idk it is different.
480 		//
481 		// then the temp is microseconds per quarter note.
482 
483 		auto time = (cast(long) minWait * tempo / file.timing).usecs;
484 		midiClock += minWait;
485 
486 		pending = PlayStreamEvent(time, trackPositions[minWaitTrack].remaining[0], file, trackPositions[minWaitTrack].track, midiClock);
487 		processPending();
488 		trackPositions[minWaitTrack].remaining = trackPositions[minWaitTrack].remaining[1 .. $];
489 		trackPositions[minWaitTrack].clock = 0;
490 		currentTrack = minWaitTrack;
491 
492 		return;
493 	}
494 
495 	private struct TrackPosition {
496 		MidiEvent[] remaining;
497 		int clock;
498 		MidiTrack track;
499 	}
500 	private TrackPosition[] trackPositions;
501 	private int currentTrack;
502 
503 	private void processPending() {
504 		if(pending.event.status == 0xff && pending.event.data1 == MetaEvent.Tempo) {
505 			this.tempo = 0;
506 			foreach(i; pending.event.meta) {
507 				this.tempo <<= 8;
508 				this.tempo |= i;
509 			}
510 		}
511 	}
512 
513 	@property
514 	PlayStreamEvent front() {
515 		return pending;
516 	}
517 
518 	private uint tempo;
519 	private PlayStreamEvent pending;
520 	private bool done;
521 
522 	@property
523 	bool empty() {
524 		return done;
525 	}
526 
527 }
528 
529 class MidiTrack {
530 	ubyte[] toBytes() {
531 		MidiWriteBuffer buf;
532 		foreach(event; events)
533 			event.writeToBuffer(buf);
534 
535 		MidiEvent end;
536 		end.status = 0xff;
537 		end.data1 = 0x2f;
538 		end.meta = null;
539 
540 		end.writeToBuffer(buf);
541 
542 		return buf.bytes;
543 	}
544 
545 	void loadFromBuffer(ref MidiReadBuffer buf) {
546 		if(buf.readChars(4) != "MTrk")
547 			throw new Exception("wtf no track header");
548 
549 		auto trackLength = buf.read4();
550 		auto begin = buf.bytes.length;
551 
552 		ubyte runningStatus;
553 
554 		while(buf.bytes.length) {
555 			MidiEvent newEvent = MidiEvent.fromBuffer(buf, runningStatus);
556 
557 			if(newEvent.isMeta && newEvent.data1 == MetaEvent.Name)
558 				name_ = cast(string) newEvent.meta.idup;
559 
560 			if(newEvent.status == 0xff && newEvent.data1 == MetaEvent.EndOfTrack) {
561 				break;
562 			}
563 			events ~= newEvent;
564 		}
565 		//assert(begin - trackLength == buf.bytes.length);
566 	}
567 
568 	/++
569 		All the midi events found in the track.
570 	+/
571 	MidiEvent[] events;
572 	/++
573 		The name of the track, as found from metadata at load time.
574 
575 		This may change to scan events to see updates without the cache in the future.
576 	+/
577 	@property string name() {
578 		return name_;
579 	}
580 
581 	private string name_;
582 
583 	/++
584 		This field is not used or stored in a midi file; it is just
585 		a place to store some state in your player.
586 
587 		I use it to keep flags like if the track is currently enabled.
588 	+/
589 	int customPlayerInfo;
590 
591 	override string toString() const {
592 		string s;
593 		foreach(event; events)
594 			s ~= event.toString ~ "\n";
595 		return s;
596 	}
597 }
598 
599 enum MetaEvent {
600 	SequenceNumber = 0,
601 	// these take a text param
602 	Text = 1,
603 	Copyright = 2,
604 	Name = 3,
605 	Instrument = 4,
606 	Lyric = 5,
607 	Marker = 6,
608 	CuePoint = 7,
609 	PatchName = 8,
610 	DeviceName = 9,
611 
612 	// no param
613 	EndOfTrack = 0x2f,
614 
615 	// different ones
616 	Tempo = 0x51, // 3 bytes form big-endian micro-seconds per quarter note. 120 BPM default.
617 	SMPTEOffset = 0x54, // 5 bytes. I don't get this one....
618 	TimeSignature = 0x58, // 4 bytes: numerator, denominator, clocks per click, 32nd notes per quarter note. (8 == quarter note gets the beat)
619 	KeySignature = 0x59, // 2 bytes: first byte is signed offset from C in semitones, second byte is 0 for major, 1 for minor
620 
621 	// arbitrary length custom param
622 	Proprietary = 0x7f,
623 
624 }
625 
626 struct MidiEvent {
627 	int deltaTime;
628 
629 	ubyte status;
630 
631 	ubyte data1; // if meta, this is the identifier
632 
633 	//union {
634 		//struct {
635 			ubyte data2;
636 		//}
637 
638 		const(ubyte)[] meta; // iff status == 0xff
639 	//}
640 
641 	invariant () {
642 		assert(status & 0x80);
643 		assert(!(data1 & 0x80));
644 		assert(!(data2 & 0x80));
645 		assert(status == 0xff || meta is null);
646 	}
647 
648 	/// Convenience factories for various meta-events
649 	static MidiEvent Text(string t) { return MidiEvent(0, 0xff, MetaEvent.Text, 0, cast(const(ubyte)[]) t); }
650 	/// ditto
651 	static MidiEvent Copyright(string t) { return MidiEvent(0, 0xff, MetaEvent.Copyright, 0, cast(const(ubyte)[]) t); }
652 	/// ditto
653 	static MidiEvent Name(string t) { return MidiEvent(0, 0xff, MetaEvent.Name, 0, cast(const(ubyte)[]) t); }
654 	/// ditto
655 	static MidiEvent Lyric(string t) { return MidiEvent(0, 0xff, MetaEvent.Lyric, 0, cast(const(ubyte)[]) t); }
656 	/// ditto
657 	static MidiEvent Marker(string t) { return MidiEvent(0, 0xff, MetaEvent.Marker, 0, cast(const(ubyte)[]) t); }
658 	/// ditto
659 	static MidiEvent CuePoint(string t) { return MidiEvent(0, 0xff, MetaEvent.CuePoint, 0, cast(const(ubyte)[]) t); }
660 
661 	/++
662 		Conveneince factories for normal events. These just put your given values into the event as raw data so you're responsible to know what they do.
663 
664 		History:
665 			Added January 2, 2022 (dub v10.5)
666 	+/
667 	static MidiEvent NoteOn(int channel, int note, int velocity) { return MidiEvent(0, (MIDI_EVENT_NOTE_ON << 4) | (channel & 0x0f), note & 0x7f, velocity & 0x7f); }
668 	/// ditto
669 	static MidiEvent NoteOff(int channel, int note, int velocity) { return MidiEvent(0, (MIDI_EVENT_NOTE_OFF << 4) | (channel & 0x0f), note & 0x7f, velocity & 0x7f); }
670 
671 	/+
672 	// FIXME: this is actually a relatively complicated one i should fix, it combines bits... 8192 == 0.
673 	// This is a bit of a magical function, it takes a signed bend between 0 and 81
674 	static MidiEvent PitchBend(int channel, int bend) {
675 		return MidiEvent(0, (MIDI_EVENT_PITCH_BEND << 4) | (channel & 0x0f), bend & 0x7f, bend & 0x7f);
676 	}
677 	+/
678 	// this overload ok, it is what the thing actually tells. coarse == 64 means we're at neutral.
679 	/// ditto
680 	static MidiEvent PitchBend(int channel, int fine, int coarse) { return MidiEvent(0, (MIDI_EVENT_PITCH_BEND << 4) | (channel & 0x0f), fine & 0x7f, coarse & 0x7f); }
681 
682 	/// ditto
683 	static MidiEvent NoteAftertouch(int channel, int note, int velocity) { return MidiEvent(0, (MIDI_EVENT_NOTE_AFTERTOUCH << 4) | (channel & 0x0f), note & 0x7f, velocity & 0x7f); }
684 	// FIXME the different controllers do have standard IDs we could look up in an enum... and many of them have coarse/fine things you can send as two messages.
685 	/// ditto
686 	static MidiEvent Controller(int channel, int controller, int value) { return MidiEvent(0, (MIDI_EVENT_CONTROLLER << 4) | (channel & 0x0f), controller & 0x7f, value & 0x7f); }
687 
688 	// the two byte ones
689 	/// ditto
690 	static MidiEvent ProgramChange(int channel, int program) { return MidiEvent(0, (MIDI_EVENT_PROGRAM_CHANGE << 4) | (channel & 0x0f), program & 0x7f); }
691 	/// ditto
692 	static MidiEvent ChannelAftertouch(int channel, int param) { return MidiEvent(0, (MIDI_EVENT_CHANNEL_AFTERTOUCH << 4) | (channel & 0x0f), param & 0x7f); }
693 
694 	///
695 	bool isMeta() const {
696 		return status == 0xff;
697 	}
698 
699 	///
700 	ubyte event() const {
701 		return status >> 4;
702 	}
703 
704 	///
705 	ubyte channel() const {
706 		return status & 0x0f;
707 	}
708 
709 	/++
710 		Returns true if it is either note off or note on with zero velocity, both of which should silence the note.
711 
712 		History:
713 			Added July 20, 2025
714 	+/
715 	bool isNoteOff() const {
716 		// data1 is the note fyi
717 		return this.event == MIDI_EVENT_NOTE_OFF || (this.event == MIDI_EVENT_NOTE_ON && this.data2 == 0);
718 	}
719 
720 	/++
721 		Returns true if it is a note on with non-zero velocity, which should sound a note.
722 
723 		History:
724 			Added July 20, 2025
725 
726 	+/
727 	bool isNoteOn() const {
728 		return this.event == MIDI_EVENT_NOTE_ON && this.data2 != 0;
729 	}
730 
731 	///
732 	string toString() const {
733 
734 		static string tos(int a) {
735 			char[16] buffer;
736 			auto bufferPos = buffer.length;
737 			do {
738 				buffer[--bufferPos] = a % 10 + '0';
739 				a /= 10;
740 			} while(a);
741 
742 			return buffer[bufferPos .. $].idup;
743 		}
744 
745 		static string toh(ubyte b) {
746 			char[2] buffer;
747 			buffer[0] = (b >> 4) & 0x0f;
748 			if(buffer[0] < 10)
749 				buffer[0] += '0';
750 			else
751 				buffer[0] += 'A' - 10;
752 			buffer[1] = b & 0x0f;
753 			if(buffer[1] < 10)
754 				buffer[1] += '0';
755 			else
756 				buffer[1] += 'A' - 10;
757 
758 			return buffer.idup;
759 		}
760 
761 		string s;
762 		s ~= tos(deltaTime);
763 		s ~= ": ";
764 		s ~= toh(status);
765 		s ~= " ";
766 		s ~= toh(data1);
767 		s ~= " ";
768 
769 		if(isMeta) {
770 			switch(data1) {
771 				case MetaEvent.Text:
772 				case MetaEvent.Copyright:
773 				case MetaEvent.Name:
774 				case MetaEvent.Instrument:
775 				case MetaEvent.Lyric:
776 				case MetaEvent.Marker:
777 				case MetaEvent.CuePoint:
778 				case MetaEvent.PatchName:
779 				case MetaEvent.DeviceName:
780 					s ~= cast(const(char)[]) meta;
781 				break;
782 				case MetaEvent.TimeSignature:
783 					ubyte numerator = meta[0];
784 					ubyte denominator = meta[1];
785 					ubyte clocksPerClick = meta[2];
786 					ubyte notesPerQuarter = meta[3]; // 32nd notes / Q so 8 = quarter note gets the beat
787 
788 					s ~= tos(numerator);
789 					s ~= "/";
790 					s ~= tos(denominator);
791 					s ~= " ";
792 					s ~= tos(clocksPerClick);
793 					s ~= " ";
794 					s ~= tos(notesPerQuarter);
795 				break;
796 				case MetaEvent.KeySignature:
797 					byte offset = meta[0];
798 					ubyte minor = meta[1];
799 
800 					if(offset < 0) {
801 						s ~= "-";
802 						s ~= tos(-cast(int) offset);
803 					} else {
804 						s ~= tos(offset);
805 					}
806 					s ~= minor ? " minor" : " major";
807 				break;
808 				// case MetaEvent.Tempo:
809 					// could process this but idk if it needs to be shown
810 				// break;
811 				case MetaEvent.Proprietary:
812 					foreach(m; meta) {
813 						s ~= toh(m);
814 						s ~= " ";
815 					}
816 				break;
817 				default:
818 					s ~= cast(const(char)[]) meta;
819 			}
820 		} else {
821 			s ~= toh(data2);
822 
823 			s ~= " ";
824 			s ~= tos(channel);
825 			s ~= " ";
826 			switch(event) {
827 				case MIDI_EVENT_NOTE_OFF: s ~= "NOTE_OFF"; break;
828 				case MIDI_EVENT_NOTE_ON: s ~=  data2 ? "NOTE_ON" : "NOTE_ON_ZERO"; break;
829 				case MIDI_EVENT_NOTE_AFTERTOUCH: s ~= "NOTE_AFTERTOUCH"; break;
830 				case MIDI_EVENT_CONTROLLER: s ~= "CONTROLLER"; break;
831 				case MIDI_EVENT_PROGRAM_CHANGE: s ~= "PROGRAM_CHANGE"; break;
832 				case MIDI_EVENT_CHANNEL_AFTERTOUCH: s ~= "CHANNEL_AFTERTOUCH"; break;
833 				case MIDI_EVENT_PITCH_BEND: s ~= "PITCH_BEND"; break;
834 				default:
835 			}
836 		}
837 
838 		return s;
839 	}
840 
841 	static MidiEvent fromBuffer(ref MidiReadBuffer buf, ref ubyte runningStatus) {
842 		MidiEvent event;
843 
844 		start_over:
845 
846 		event.deltaTime = buf.readv();
847 
848 		auto nb = buf.read1();
849 
850 		if(nb == 0xff) {
851 			// meta...
852 			event.status = 0xff;
853 			event.data1 = buf.read1(); // the type
854 			int len = buf.readv();
855 			auto meta = new ubyte[](len);
856 			foreach(idx; 0 .. len)
857 				meta[idx] = buf.read1();
858 			event.meta = meta;
859 		} else if(nb >= 0xf0) {
860 			// FIXME I'm just skipping this entirely but there might be value in here
861 			nb = buf.read1();
862 			while(nb < 0xf0)
863 				nb = buf.read1();
864 			goto start_over;
865 		} else if(nb & 0b1000_0000) {
866 			event.status = nb;
867 			runningStatus = nb;
868 			event.data1 = buf.read1();
869 
870 			if(event.event != MIDI_EVENT_CHANNEL_AFTERTOUCH &&
871 				event.event != MIDI_EVENT_PROGRAM_CHANGE)
872 			{
873 				event.data2 = buf.read1();
874 			}
875 		} else {
876 			event.status = runningStatus;
877 			event.data1 = nb;
878 
879 			if(event.event != MIDI_EVENT_CHANNEL_AFTERTOUCH &&
880 				event.event != MIDI_EVENT_PROGRAM_CHANGE)
881 			{
882 				event.data2 = buf.read1();
883 			}
884 		}
885 
886 		return event;
887 	}
888 
889 	void writeToBuffer(ref MidiWriteBuffer buf) const {
890 		buf.writev(deltaTime);
891 		buf.write1(status);
892 		// FIXME: what about other sysex stuff?
893 		if(meta) {
894 			buf.write1(data1);
895 			buf.writev(cast(int) meta.length);
896 			buf.write(meta);
897 		} else {
898 			buf.write1(data1);
899 
900 			if(event != MIDI_EVENT_CHANNEL_AFTERTOUCH &&
901 				event != MIDI_EVENT_PROGRAM_CHANGE)
902 			{
903 				buf.write1(data2);
904 			}
905 		}
906 	}
907 }
908 
909 struct MidiReadBuffer {
910 	ubyte[] bytes;
911 
912 	char[] readChars(int len) {
913 		auto c = bytes[0 .. len];
914 		bytes = bytes[len .. $];
915 		return cast(char[]) c;
916 	}
917 	ubyte[] readBytes(int len) {
918 		auto c = bytes[0 .. len];
919 		bytes = bytes[len .. $];
920 		return c;
921 	}
922 	int read4() {
923 		int i;
924 		foreach(a; 0 .. 4) {
925 			i <<= 8;
926 			i |= bytes[0];
927 			bytes = bytes[1 .. $];
928 		}
929 		return i;
930 	}
931 	ushort read2() {
932 		ushort i;
933 		foreach(a; 0 .. 2) {
934 			i <<= 8;
935 			i |= bytes[0];
936 			bytes = bytes[1 .. $];
937 		}
938 		return i;
939 	}
940 	ubyte read1() {
941 		auto b = bytes[0];
942 		bytes = bytes[1 .. $];
943 		return b;
944 	}
945 	int readv() {
946 		int value = read1();
947 		ubyte c;
948 		if(value & 0x80) {
949 			value &= 0x7f;
950 			do
951 				value = (value << 7) | ((c = read1) & 0x7f);
952 			while(c & 0x80);
953 		}
954 		return value;
955 	}
956 }
957 
958 struct MidiWriteBuffer {
959 	ubyte[] bytes;
960 
961 	void write(const char[] a) {
962 		bytes ~= a;
963 	}
964 
965 	void write(const ubyte[] a) {
966 		bytes ~= a;
967 	}
968 
969 	void write4(int v) {
970 		// big endian
971 		bytes ~= (v >> 24) & 0xff;
972 		bytes ~= (v >> 16) & 0xff;
973 		bytes ~= (v >> 8) & 0xff;
974 		bytes ~= v & 0xff;
975 	}
976 
977 	void write2(ushort v) {
978 		// big endian
979 		bytes ~= v >> 8;
980 		bytes ~= v & 0xff;
981 	}
982 
983 	void write1(ubyte v) {
984 		bytes ~= v;
985 	}
986 
987 	void writev(int v) {
988 		// variable
989 		uint buffer = v & 0x7f;
990 		while((v >>= 7)) {
991 			buffer <<= 8;
992 			buffer |= ((v & 0x7f) | 0x80);
993 		}
994 
995 		while(true) {
996 			bytes ~= buffer & 0xff;
997 			if(buffer & 0x80)
998 				buffer >>= 8;
999 			else
1000 				break;
1001 		}
1002 	}
1003 }
1004 
1005 import core.stdc.stdio;
1006 import core.stdc.stdlib;
1007 
1008 int freq(int note){
1009 	import std.math;
1010 	float r = note - 69;
1011 	r /= 12;
1012 	r = pow(2, r);
1013 	r*= 440;
1014 	return cast(int) r;
1015 }
1016 
1017 enum A =  69; // 440 hz per midi spec
1018 enum As = 70;
1019 enum B =  71;
1020 enum C =  72; // middle C + 1 octave
1021 enum Cs = 73;
1022 enum D =  74;
1023 enum Ds = 75;
1024 enum E =  76;
1025 enum F =  77;
1026 enum Fs = 78;
1027 enum G =  79;
1028 enum Gs = 80;
1029 
1030 immutable string[] noteNames = [ // just do note % 12 to index this
1031         "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
1032 ];
1033 
1034 enum MIDI_EVENT_NOTE_OFF =		0x08;
1035 enum MIDI_EVENT_NOTE_ON =		0x09;
1036 enum MIDI_EVENT_NOTE_AFTERTOUCH =	0x0a;
1037 enum MIDI_EVENT_CONTROLLER =		0x0b;
1038 enum MIDI_EVENT_PROGRAM_CHANGE =	0x0c;// only one param
1039 enum MIDI_EVENT_CHANNEL_AFTERTOUCH =	0x0d;// only one param
1040 enum MIDI_EVENT_PITCH_BEND =		0x0e;
1041 
1042 
1043  /+
1044  35   Acoustic Bass Drum     59   Ride Cymbal 2
1045  36   Bass Drum 1            60   Hi Bongo
1046  37   Side Stick             61   Low Bongo
1047  38   Acoustic Snare         62   Mute Hi Conga
1048  39   Hand Clap              63   Open Hi Conga
1049  40   Electric Snare         64   Low Conga
1050  41   Low Floor Tom          65   High Timbale
1051  42   Closed Hi-Hat          66   Low Timbale
1052  43   High Floor Tom         67   High Agogo
1053  44   Pedal Hi-Hat           68   Low Agogo
1054  45   Low Tom                69   Cabasa
1055  46   Open Hi-Hat            70   Maracas
1056  47   Low-Mid Tom            71   Short Whistle
1057  48   Hi-Mid Tom             72   Long Whistle
1058  49   Crash Cymbal 1         73   Short Guiro
1059  50   High Tom               74   Long Guiro
1060  51   Ride Cymbal 1          75   Claves
1061  52   Chinese Cymbal         76   Hi Wood Block
1062  53   Ride Bell              77   Low Wood Block
1063  54   Tambourine             78   Mute Cuica
1064  55   Splash Cymbal          79   Open Cuica
1065  56   Cowbell                80   Mute Triangle
1066  57   Crash Cymbal 2         81   Open Triangle
1067  58   Vibraslap
1068  +/
1069 
1070 static immutable string[] instrumentNames = [
1071 "", // 0 is nothing
1072 // Piano:
1073 "Acoustic Grand Piano",
1074 "Bright Acoustic Piano",
1075 "Electric Grand Piano",
1076 "Honky-tonk Piano",
1077 "Electric Piano 1",
1078 "Electric Piano 2",
1079 "Harpsichord",
1080 "Clavinet",
1081 
1082 // Chromatic Percussion:
1083 "Celesta",
1084 "Glockenspiel",
1085 "Music Box",
1086 "Vibraphone",
1087 "Marimba",
1088 "Xylophone",
1089 "Tubular Bells",
1090 "Dulcimer",
1091 
1092 // Organ:
1093 "Drawbar Organ",
1094 "Percussive Organ",
1095 "Rock Organ",
1096 "Church Organ",
1097 "Reed Organ",
1098 "Accordion",
1099 "Harmonica",
1100 "Tango Accordion",
1101 
1102 // Guitar:
1103 "Acoustic Guitar (nylon)",
1104 "Acoustic Guitar (steel)",
1105 "Electric Guitar (jazz)",
1106 "Electric Guitar (clean)",
1107 "Electric Guitar (muted)",
1108 "Overdriven Guitar",
1109 "Distortion Guitar",
1110 "Guitar harmonics",
1111 
1112 // Bass:
1113 "Acoustic Bass",
1114 "Electric Bass (finger)",
1115 "Electric Bass (pick)",
1116 "Fretless Bass",
1117 "Slap Bass 1",
1118 "Slap Bass 2",
1119 "Synth Bass 1",
1120 "Synth Bass 2",
1121 
1122 // Strings:
1123 "Violin",
1124 "Viola",
1125 "Cello",
1126 "Contrabass",
1127 "Tremolo Strings",
1128 "Pizzicato Strings",
1129 "Orchestral Harp",
1130 "Timpani",
1131 
1132 // Strings (continued):
1133 "String Ensemble 1",
1134 "String Ensemble 2",
1135 "Synth Strings 1",
1136 "Synth Strings 2",
1137 "Choir Aahs",
1138 "Voice Oohs",
1139 "Synth Voice",
1140 "Orchestra Hit",
1141 
1142 // Brass:
1143 "Trumpet",
1144 "Trombone",
1145 "Tuba",
1146 "Muted Trumpet",
1147 "French Horn",
1148 "Brass Section",
1149 "Synth Brass 1",
1150 "Synth Brass 2",
1151 
1152 // Reed:
1153 "Soprano Sax",
1154 "Alto Sax",
1155 "Tenor Sax",
1156 "Baritone Sax",
1157 "Oboe",
1158 "English Horn",
1159 "Bassoon",
1160 "Clarinet",
1161 
1162 // Pipe:
1163 "Piccolo",
1164 "Flute",
1165 "Recorder",
1166 "Pan Flute",
1167 "Blown Bottle",
1168 "Shakuhachi",
1169 "Whistle",
1170 "Ocarina",
1171 
1172 // Synth Lead:
1173 "Lead 1 (square)",
1174 "Lead 2 (sawtooth)",
1175 "Lead 3 (calliope)",
1176 "Lead 4 (chiff)",
1177 "Lead 5 (charang)",
1178 "Lead 6 (voice)",
1179 "Lead 7 (fifths)",
1180 "Lead 8 (bass + lead)",
1181 
1182 // Synth Pad:
1183 "Pad 1 (new age)",
1184 "Pad 2 (warm)",
1185 "Pad 3 (polysynth)",
1186 "Pad 4 (choir)",
1187 "Pad 5 (bowed)",
1188 "Pad 6 (metallic)",
1189 "Pad 7 (halo)",
1190 "Pad 8 (sweep)",
1191 
1192 // Synth Effects:
1193 "FX 1 (rain)",
1194 "FX 2 (soundtrack)",
1195 "FX 3 (crystal)",
1196 "FX 4 (atmosphere)",
1197 "FX 5 (brightness)",
1198 "FX 6 (goblins)",
1199 "FX 7 (echoes)",
1200 "FX 8 (sci-fi)",
1201 
1202 // Ethnic:
1203 "Sitar",
1204 "Banjo",
1205 "Shamisen",
1206 "Koto",
1207 "Kalimba",
1208 "Bag pipe",
1209 "Fiddle",
1210 "Shanai",
1211 
1212 // Percussive:
1213 "Tinkle Bell",
1214 "Agogo",
1215 "Steel Drums",
1216 "Woodblock",
1217 "Taiko Drum",
1218 "Melodic Tom",
1219 "Synth Drum",
1220 
1221 // Sound effects:
1222 "Reverse Cymbal",
1223 "Guitar Fret Noise",
1224 "Breath Noise",
1225 "Seashore",
1226 "Bird Tweet",
1227 "Telephone Ring",
1228 "Helicopter",
1229 "Applause",
1230 "Gunshot"
1231 ];
1232 
1233 version(MidiDemo) {
1234 
1235 
1236 enum SKIP_MAX = 3000; // allow no more than about 3 seconds of silence
1237 			 // if the -k option is set
1238 
1239 // Potential FIXME: it doesn't support more than 128 tracks.
1240 
1241 void awesome(void* midiptr, int note, int wait) {
1242 	printf("%d %d ", wait, note);
1243 	fflush(stdout);
1244 }
1245 
1246 // FIXME: add support for displaying lyrics
1247 extern(C) int main(int argc, char** argv){
1248 
1249 	for(a = 1; a < argc; a++){
1250 		if(argv[a][0] == '-')
1251 		switch(argv[a][1]){
1252 			case 't':
1253 				for(b = 0; b< 128; b++)
1254 					playtracks[b] = 0;
1255 				num = 0;
1256 				b = 0;
1257 				a++;
1258 				if(a == argc){
1259 					printf("%s: option %s requires an argument\n", argv[0], argv[a-1]);
1260 					return 1;
1261 				}
1262 				for(b = 0; argv[a][b]; b++){
1263 					if(argv[a][b] == ','){
1264 						playtracks[num] = 1;
1265 						num = 0;
1266 						continue;
1267 					}
1268 					num *= 10;
1269 					num += argv[a][b] - '0';
1270 				}
1271 				playtracks[num] = 1;
1272 			break;
1273 			case 's':
1274 				a++;
1275 				if(a == argc){
1276 					printf("%s: option %s requires an argument\n", argv[0], argv[a-1]);
1277 					return 1;
1278 				}
1279 				tempoMultiplier = atof(argv[a]);
1280 			break;
1281 			case 'i': // FIXME
1282 				displayinfo = 1;
1283 				// tracks, guesstimated length
1284 			break;
1285 			// -o loop to from
1286 			// -b begin at
1287 			// -e end at
1288 			case 'l':
1289 				tracing = 1;
1290 			break;
1291 			case 'n':
1292 				play = 0;
1293 			break;
1294 			case 'k':
1295 				skip = 1;
1296 			break;
1297 			case 'c':
1298 				channelMask = 0;
1299 				// channels
1300 				num = 0;
1301 				b = 0;
1302 				a++;
1303 				if(a == argc){
1304 					printf("%s: option %s requires an argument\n", argv[0], argv[a-1]);
1305 					return 1;
1306 				}
1307 				for(b = 0; argv[a][b]; b++){
1308 					if(argv[a][b] == ','){
1309 						channelMask |= (1 << num);
1310 						num = 0;
1311 						continue;
1312 					}
1313 					num *= 10;
1314 					num += argv[a][b] - '0';
1315 				}
1316 					channelMask |= (1 << num);
1317 			break;
1318 			case 'r':
1319 				a++;
1320 				if(a == argc){
1321 					printf("%s: option %s requires an argument\n", argv[0], argv[a-1]);
1322 					return 1;
1323 				}
1324 				transpose = atoi(argv[a]);
1325 			break;
1326 			case 'v':
1327 				verbose = 1;
1328 			break;
1329 			case 'h':
1330 				printf("Usage: %s [options...] file\n", argv[0]);
1331 				printf("  Options:\n");
1332 				printf("  -t comma separated list of tracks to play (default: all)\n");
1333 				printf("  -s tempo (speed) multiplier (default: 1.0)\n");
1334 				printf("  -i file info (track list)\n");
1335 				printf("  -l list notes as they are played (in the format totablature expects)\n");
1336 				printf("  -n no sound; don't actually play the midi\n");
1337 				printf("  -c comma separated list of channels to play (default: all)\n");
1338 				printf("  -r transpose notes by amount (default: 0)\n");
1339 				printf("  -k skip long sections of silence (good for playing single tracks)\n");
1340 
1341 				printf("  -v verbose; list all events except note on / note off\n");
1342 				printf("  -h shows this help screen\n");
1343 
1344 				return 0;
1345 			break;
1346 			default:
1347 				printf("%s: unknown command line option: %s\n", argv[0], argv[1]);
1348 				return 1;
1349 		}
1350 		else
1351 			filename = argv[a];
1352 	}
1353 
1354 	if(filename == null){
1355 		printf("%s: no file given. Try %s -h for help.\n", argv[0], argv[0]);
1356 		return 1;
1357 	}
1358 
1359 	loadMidi(&mid, filename);
1360 	if(mid == null){
1361 		printf("%s: unable to read file %s\n", argv[0], filename);
1362 		return 1;
1363 	}
1364 
1365 	if(displayinfo){
1366 		int len = getMidiLength(mid);
1367 		printf("File: %s\n", filename);
1368 		printf("Ticks per quarter note: %d\n", mid.speed);
1369 		printf("Initial tempo: %d\n", getMidiTempo(mid));
1370 		printf("Length: %d:%d\n", len / 60, len%60);
1371 		printf("Tracks:\n");
1372 		for(a = 0; a < mid.numTracks; a++){
1373 			c[0] = getTrackNameChunk(mid, a);
1374 			if(c[0] != null){
1375 				printf("%d: ", a);
1376 				for(b = 0; b < c[0].length; b++)
1377 					fputc(c[0].data[b], stdout);
1378 				printf("\n");
1379 			}
1380 		}
1381 
1382 		freeMidi(&mid);
1383 		return 0;
1384 	}
1385 
1386 	return 0;
1387 }
1388 }