/** 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. I'll eventually refactor it into something more D-like History: Written in C in August 2008 Minimally ported to D in September 2017 Updated May 2020 with significant changes. */ module arsd.midi; import core.time; version(NewMidiDemo) void main(string[] args) { auto f = new MidiFile(); import std.file; //f.loadFromBytes(cast(ubyte[]) read("test.mid")); f.loadFromBytes(cast(ubyte[]) read(args[1])); import arsd.simpleaudio; import core.thread; auto o = MidiOutput(0); setSigIntHandler(); scope(exit) { o.silenceAllNotes(); o.reset(); restoreSigIntHandler(); } import std.stdio : writeln; foreach(item; f.playbackStream) { if(interrupted) return; Thread.sleep(item.wait); if(!item.event.isMeta) o.writeMidiMessage(item.event.status, item.event.data1, item.event.data2); else writeln(item); } return; auto t = new MidiTrack(); auto t2 = new MidiTrack(); f.tracks ~= t; f.tracks ~= t2; t.events ~= MidiEvent(0, 0x90, C, 127); t.events ~= MidiEvent(256, 0x90, C, 0); t.events ~= MidiEvent(256, 0x90, D, 127); t.events ~= MidiEvent(256, 0x90, D, 0); t.events ~= MidiEvent(256, 0x90, E, 127); t.events ~= MidiEvent(256, 0x90, E, 0); t.events ~= MidiEvent(256, 0x90, F, 127); t.events ~= MidiEvent(0, 0xff, 0x05, 0 /* unused */, ['h', 'a', 'm']); t.events ~= MidiEvent(256, 0x90, F, 0); t2.events ~= MidiEvent(0, (MIDI_EVENT_PROGRAM_CHANGE << 4) | 0x01, 68); t2.events ~= MidiEvent(128, 0x91, E, 127); t2.events ~= MidiEvent(0, 0xff, 0x05, 0 /* unused */, ['a', 'd', 'r']); t2.events ~= MidiEvent(1024, 0x91, E, 0); write("test.mid", f.toBytes()); } @safe: class MidiFile { /// ubyte[] toBytes() { MidiWriteBuffer buf; buf.write("MThd"); buf.write4(6); buf.write2(format); buf.write2(cast(ushort) tracks.length); buf.write2(timing); foreach(track; tracks) { auto data = track.toBytes(); buf.write("MTrk"); buf.write4(cast(int) data.length); buf.write(data); } return buf.bytes; } /// void loadFromBytes(ubyte[] bytes) { // FIXME: actually read the riff header to skip properly if(bytes.length && bytes[0] == 'R') bytes = bytes[0x14 .. $]; MidiReadBuffer buf = MidiReadBuffer(bytes); if(buf.readChars(4) != "MThd") throw new Exception("not midi"); if(buf.read4() != 6) throw new Exception("idk what this even is"); this.format = buf.read2(); this.tracks = new MidiTrack[](buf.read2()); this.timing = buf.read2(); foreach(ref track; tracks) { track = new MidiTrack(); track.loadFromBuffer(buf); } } // when I read, I plan to cut the end of track marker off. // 0 == combined into one track // 1 == multiple tracks // 2 == multiple one-track patterns ushort format = 1; // FIXME ushort timing = 0x80; // 128 ticks per quarter note MidiTrack[] tracks; /++ Returns a forward range for playback. Each item is a command, which is like the midi event but with some more annotations and control methods. Modifying this MidiFile object or any of its children during playback may cause trouble. Note that you do not need to handle any meta events, it keeps the tempo internally, but you can look at it if you like. +/ PlayStream playbackStream() { return PlayStream(this); } } struct PlayStream { static struct Event { /// This is how long you wait until triggering this event. /// Note it may be zero. Duration wait; /// And this is the event. MidiEvent event; string toString() { return event.toString(); } /// informational MidiFile file; /// ditto MidiTrack track; } PlayStream save() { auto copy = this; copy.trackPositions = this.trackPositions.dup; return copy; } MidiFile file; this(MidiFile file) { this.file = file; this.trackPositions.length = file.tracks.length; foreach(idx, ref tp; this.trackPositions) { tp.remaining = file.tracks[idx].events[]; tp.track = file.tracks[idx]; } this.currentTrack = -1; this.tempo = 500000; popFront(); } //@nogc: void popFront() { done = true; for(auto c = currentTrack + 1; c < trackPositions.length; c++) { auto tp = trackPositions[c]; if(tp.remaining.length && tp.remaining[0].deltaTime == tp.clock) { auto f = tp.remaining[0]; trackPositions[c].remaining = tp.remaining[1 .. $]; trackPositions[c].clock = 0; if(tp.remaining.length == 0 || tp.remaining[0].deltaTime > 0) { currentTrack += 1; } pending = Event(0.seconds, f, file, tp.track); processPending(); done = false; return; } } // if nothing happened there, time to advance the clock int minWait = int.max; int minWaitTrack = -1; foreach(idx, track; trackPositions) { if(track.remaining.length) { auto dt = track.remaining[0].deltaTime - track.clock; if(dt < minWait) { minWait = dt; minWaitTrack = cast(int) idx; } } } if(minWaitTrack == -1) { done = true; return; } foreach(ref tp; trackPositions) { tp.clock += minWait; } done = false; // file.timing, if high bit clear, is ticks per quarter note // if high bit set... idk it is different. // // then the temp is microseconds per quarter note. auto time = (minWait * tempo / file.timing).usecs; pending = Event(time, trackPositions[minWaitTrack].remaining[0], file, trackPositions[minWaitTrack].track); processPending(); trackPositions[minWaitTrack].remaining = trackPositions[minWaitTrack].remaining[1 .. $]; trackPositions[minWaitTrack].clock = 0; currentTrack = minWaitTrack; return; } private struct TrackPosition { MidiEvent[] remaining; int clock; MidiTrack track; } private TrackPosition[] trackPositions; private int currentTrack; private void processPending() { if(pending.event.status == 0xff && pending.event.data1 == MetaEvent.Tempo) { this.tempo = 0; foreach(i; pending.event.meta) { this.tempo <<= 8; this.tempo |= i; } } } @property Event front() { return pending; } private uint tempo; private Event pending; private bool done; @property bool empty() { return done; } } class MidiTrack { ubyte[] toBytes() { MidiWriteBuffer buf; foreach(event; events) event.writeToBuffer(buf); MidiEvent end; end.status = 0xff; end.data1 = 0x2f; end.meta = null; end.writeToBuffer(buf); return buf.bytes; } void loadFromBuffer(ref MidiReadBuffer buf) { if(buf.readChars(4) != "MTrk") throw new Exception("wtf no track header"); auto trackLength = buf.read4(); auto begin = buf.bytes.length; ubyte runningStatus; while(buf.bytes.length) { MidiEvent newEvent = MidiEvent.fromBuffer(buf, runningStatus); if(newEvent.status == 0xff && newEvent.data1 == MetaEvent.EndOfTrack) { break; } events ~= newEvent; } //assert(begin - trackLength == buf.bytes.length); } MidiEvent[] events; override string toString() const { string s; foreach(event; events) s ~= event.toString ~ "\n"; return s; } } enum MetaEvent { SequenceNumber = 0, // these take a text param Text = 1, Copyright = 2, Name = 3, Instrument = 4, Lyric = 5, Marker = 6, CuePoint = 7, PatchName = 8, DeviceName = 9, // no param EndOfTrack = 0x2f, // different ones Tempo = 0x51, // 3 bytes form big-endian micro-seconds per quarter note. 120 BPM default. SMPTEOffset = 0x54, // 5 bytes. I don't get this one.... TimeSignature = 0x58, // 4 bytes: numerator, denominator, clocks per click, 32nd notes per quarter note. (8 == quarter note gets the beat) KeySignature = 0x59, // 2 bytes: first byte is signed offset from C in semitones, second byte is 0 for major, 1 for minor // arbitrary length custom param Proprietary = 0x7f, } struct MidiEvent { int deltaTime; ubyte status; ubyte data1; // if meta, this is the identifier //union { //struct { ubyte data2; //} const(ubyte)[] meta; // iff status == 0xff //} invariant () { assert(status & 0x80); assert(!(data1 & 0x80)); assert(!(data2 & 0x80)); assert(status == 0xff || meta is null); } /// Convenience factories for various meta-events static MidiEvent Text(string t) { return MidiEvent(0, 0xff, MetaEvent.Text, 0, cast(const(ubyte)[]) t); } /// ditto static MidiEvent Copyright(string t) { return MidiEvent(0, 0xff, MetaEvent.Copyright, 0, cast(const(ubyte)[]) t); } /// ditto static MidiEvent Name(string t) { return MidiEvent(0, 0xff, MetaEvent.Name, 0, cast(const(ubyte)[]) t); } /// ditto static MidiEvent Lyric(string t) { return MidiEvent(0, 0xff, MetaEvent.Lyric, 0, cast(const(ubyte)[]) t); } /// ditto static MidiEvent Marker(string t) { return MidiEvent(0, 0xff, MetaEvent.Marker, 0, cast(const(ubyte)[]) t); } /// ditto static MidiEvent CuePoint(string t) { return MidiEvent(0, 0xff, MetaEvent.CuePoint, 0, cast(const(ubyte)[]) t); } /// bool isMeta() const { return status == 0xff; } /// ubyte event() const { return status >> 4; } /// ubyte channel() const { return status & 0x0f; } /// string toString() const { static string tos(int a) { char[16] buffer; auto bufferPos = buffer.length; do { buffer[--bufferPos] = a % 10 + '0'; a /= 10; } while(a); return buffer[bufferPos .. $].idup; } static string toh(ubyte b) { char[2] buffer; buffer[0] = (b >> 4) & 0x0f; if(buffer[0] < 10) buffer[0] += '0'; else buffer[0] += 'A' - 10; buffer[1] = b & 0x0f; if(buffer[1] < 10) buffer[1] += '0'; else buffer[1] += 'A' - 10; return buffer.idup; } string s; s ~= tos(deltaTime); s ~= ": "; s ~= toh(status); s ~= " "; s ~= toh(data1); s ~= " "; if(isMeta) { switch(data1) { case MetaEvent.Text: case MetaEvent.Copyright: case MetaEvent.Name: case MetaEvent.Instrument: case MetaEvent.Lyric: case MetaEvent.Marker: case MetaEvent.CuePoint: case MetaEvent.PatchName: case MetaEvent.DeviceName: s ~= cast(const(char)[]) meta; break; case MetaEvent.TimeSignature: ubyte numerator = meta[0]; ubyte denominator = meta[1]; ubyte clocksPerClick = meta[2]; ubyte notesPerQuarter = meta[3]; // 32nd notes / Q so 8 = quarter note gets the beat s ~= tos(numerator); s ~= "/"; s ~= tos(denominator); s ~= " "; s ~= tos(clocksPerClick); s ~= " "; s ~= tos(notesPerQuarter); break; case MetaEvent.KeySignature: byte offset = meta[0]; ubyte minor = meta[1]; if(offset < 0) { s ~= "-"; s ~= tos(-cast(int) offset); } else { s ~= tos(offset); } s ~= minor ? " minor" : " major"; break; // case MetaEvent.Tempo: // could process this but idk if it needs to be shown // break; case MetaEvent.Proprietary: foreach(m; meta) { s ~= toh(m); s ~= " "; } break; default: s ~= cast(const(char)[]) meta; } } else { s ~= toh(data2); } return s; } static MidiEvent fromBuffer(ref MidiReadBuffer buf, ref ubyte runningStatus) { MidiEvent event; start_over: event.deltaTime = buf.readv(); auto nb = buf.read1(); if(nb == 0xff) { // meta... event.status = 0xff; event.data1 = buf.read1(); // the type int len = buf.readv(); auto meta = new ubyte[](len); foreach(idx; 0 .. len) meta[idx] = buf.read1(); event.meta = meta; } else if(nb >= 0xf0) { // FIXME I'm just skipping this entirely but there might be value in here nb = buf.read1(); while(nb < 0xf0) nb = buf.read1(); goto start_over; } else if(nb & 0b1000_0000) { event.status = nb; runningStatus = nb; event.data1 = buf.read1(); if(event.event != MIDI_EVENT_CHANNEL_AFTERTOUCH && event.event != MIDI_EVENT_PROGRAM_CHANGE) { event.data2 = buf.read1(); } } else { event.status = runningStatus; event.data1 = nb; if(event.event != MIDI_EVENT_CHANNEL_AFTERTOUCH && event.event != MIDI_EVENT_PROGRAM_CHANGE) { event.data2 = buf.read1(); } } return event; } void writeToBuffer(ref MidiWriteBuffer buf) const { buf.writev(deltaTime); buf.write1(status); // FIXME: what about other sysex stuff? if(meta) { buf.write1(data1); buf.writev(cast(int) meta.length); buf.write(meta); } else { buf.write1(data1); if(event != MIDI_EVENT_CHANNEL_AFTERTOUCH && event != MIDI_EVENT_PROGRAM_CHANGE) { buf.write1(data2); } } } } struct MidiReadBuffer { ubyte[] bytes; char[] readChars(int len) { auto c = bytes[0 .. len]; bytes = bytes[len .. $]; return cast(char[]) c; } ubyte[] readBytes(int len) { auto c = bytes[0 .. len]; bytes = bytes[len .. $]; return c; } int read4() { int i; foreach(a; 0 .. 4) { i <<= 8; i |= bytes[0]; bytes = bytes[1 .. $]; } return i; } ushort read2() { ushort i; foreach(a; 0 .. 2) { i <<= 8; i |= bytes[0]; bytes = bytes[1 .. $]; } return i; } ubyte read1() { auto b = bytes[0]; bytes = bytes[1 .. $]; return b; } int readv() { int value = read1(); ubyte c; if(value & 0x80) { value &= 0x7f; do value = (value << 7) | ((c = read1) & 0x7f); while(c & 0x80); } return value; } } struct MidiWriteBuffer { ubyte[] bytes; void write(const char[] a) { bytes ~= a; } void write(const ubyte[] a) { bytes ~= a; } void write4(int v) { // big endian bytes ~= (v >> 24) & 0xff; bytes ~= (v >> 16) & 0xff; bytes ~= (v >> 8) & 0xff; bytes ~= v & 0xff; } void write2(ushort v) { // big endian bytes ~= v >> 8; bytes ~= v & 0xff; } void write1(ubyte v) { bytes ~= v; } void writev(int v) { // variable uint buffer = v & 0x7f; while((v >>= 7)) { buffer <<= 8; buffer |= ((v & 0x7f) | 0x80); } while(true) { bytes ~= buffer & 0xff; if(buffer & 0x80) buffer >>= 8; else break; } } } import core.stdc.stdio; import core.stdc.stdlib; int freq(int note){ import std.math; float r = note - 69; r /= 12; r = pow(2, r); r*= 440; return cast(int) r; } enum A = 69; // 440 hz per midi spec enum As = 70; enum B = 71; enum C = 72; // middle C + 1 octave enum Cs = 73; enum D = 74; enum Ds = 75; enum E = 76; enum F = 77; enum Fs = 78; enum G = 79; enum Gs = 80; immutable string[] noteNames = [ // just do note % 12 to index this "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" ]; enum MIDI_EVENT_NOTE_OFF = 0x08; enum MIDI_EVENT_NOTE_ON = 0x09; enum MIDI_EVENT_NOTE_AFTERTOUCH = 0x0a; enum MIDI_EVENT_CONTROLLER = 0x0b; enum MIDI_EVENT_PROGRAM_CHANGE = 0x0c;// only one param enum MIDI_EVENT_CHANNEL_AFTERTOUCH = 0x0d;// only one param enum MIDI_EVENT_PITCH_BEND = 0x0e; /+ 35 Acoustic Bass Drum 59 Ride Cymbal 2 36 Bass Drum 1 60 Hi Bongo 37 Side Stick 61 Low Bongo 38 Acoustic Snare 62 Mute Hi Conga 39 Hand Clap 63 Open Hi Conga 40 Electric Snare 64 Low Conga 41 Low Floor Tom 65 High Timbale 42 Closed Hi-Hat 66 Low Timbale 43 High Floor Tom 67 High Agogo 44 Pedal Hi-Hat 68 Low Agogo 45 Low Tom 69 Cabasa 46 Open Hi-Hat 70 Maracas 47 Low-Mid Tom 71 Short Whistle 48 Hi-Mid Tom 72 Long Whistle 49 Crash Cymbal 1 73 Short Guiro 50 High Tom 74 Long Guiro 51 Ride Cymbal 1 75 Claves 52 Chinese Cymbal 76 Hi Wood Block 53 Ride Bell 77 Low Wood Block 54 Tambourine 78 Mute Cuica 55 Splash Cymbal 79 Open Cuica 56 Cowbell 80 Mute Triangle 57 Crash Cymbal 2 81 Open Triangle 58 Vibraslap +/ static immutable string[] instrumentNames = [ "", // 0 is nothing // Piano: "Acoustic Grand Piano", "Bright Acoustic Piano", "Electric Grand Piano", "Honky-tonk Piano", "Electric Piano 1", "Electric Piano 2", "Harpsichord", "Clavinet", // Chromatic Percussion: "Celesta", "Glockenspiel", "Music Box", "Vibraphone", "Marimba", "Xylophone", "Tubular Bells", "Dulcimer", // Organ: "Drawbar Organ", "Percussive Organ", "Rock Organ", "Church Organ", "Reed Organ", "Accordion", "Harmonica", "Tango Accordion", // Guitar: "Acoustic Guitar (nylon)", "Acoustic Guitar (steel)", "Electric Guitar (jazz)", "Electric Guitar (clean)", "Electric Guitar (muted)", "Overdriven Guitar", "Distortion Guitar", "Guitar harmonics", // Bass: "Acoustic Bass", "Electric Bass (finger)", "Electric Bass (pick)", "Fretless Bass", "Slap Bass 1", "Slap Bass 2", "Synth Bass 1", "Synth Bass 2", // Strings: "Violin", "Viola", "Cello", "Contrabass", "Tremolo Strings", "Pizzicato Strings", "Orchestral Harp", "Timpani", // Strings (continued): "String Ensemble 1", "String Ensemble 2", "Synth Strings 1", "Synth Strings 2", "Choir Aahs", "Voice Oohs", "Synth Voice", "Orchestra Hit", // Brass: "Trumpet", "Trombone", "Tuba", "Muted Trumpet", "French Horn", "Brass Section", "Synth Brass 1", "Synth Brass 2", // Reed: "Soprano Sax", "Alto Sax", "Tenor Sax", "Baritone Sax", "Oboe", "English Horn", "Bassoon", "Clarinet", // Pipe: "Piccolo", "Flute", "Recorder", "Pan Flute", "Blown Bottle", "Shakuhachi", "Whistle", "Ocarina", // Synth Lead: "Lead 1 (square)", "Lead 2 (sawtooth)", "Lead 3 (calliope)", "Lead 4 (chiff)", "Lead 5 (charang)", "Lead 6 (voice)", "Lead 7 (fifths)", "Lead 8 (bass + lead)", // Synth Pad: "Pad 1 (new age)", "Pad 2 (warm)", "Pad 3 (polysynth)", "Pad 4 (choir)", "Pad 5 (bowed)", "Pad 6 (metallic)", "Pad 7 (halo)", "Pad 8 (sweep)", // Synth Effects: "FX 1 (rain)", "FX 2 (soundtrack)", "FX 3 (crystal)", "FX 4 (atmosphere)", "FX 5 (brightness)", "FX 6 (goblins)", "FX 7 (echoes)", "FX 8 (sci-fi)", // Ethnic: "Sitar", "Banjo", "Shamisen", "Koto", "Kalimba", "Bag pipe", "Fiddle", "Shanai", // Percussive: "Tinkle Bell", "Agogo", "Steel Drums", "Woodblock", "Taiko Drum", "Melodic Tom", "Synth Drum", // Sound effects: "Reverse Cymbal", "Guitar Fret Noise", "Breath Noise", "Seashore", "Bird Tweet", "Telephone Ring", "Helicopter", "Applause", "Gunshot" ]; version(MidiDemo) { enum SKIP_MAX = 3000; // allow no more than about 3 seconds of silence // if the -k option is set // Potential FIXME: it doesn't support more than 128 tracks. void awesome(void* midiptr, int note, int wait) { printf("%d %d ", wait, note); fflush(stdout); } // FIXME: add support for displaying lyrics extern(C) int main(int argc, char** argv){ for(a = 1; a < argc; a++){ if(argv[a][0] == '-') switch(argv[a][1]){ case 't': for(b = 0; b< 128; b++) playtracks[b] = 0; num = 0; b = 0; a++; if(a == argc){ printf("%s: option %s requires an argument\n", argv[0], argv[a-1]); return 1; } for(b = 0; argv[a][b]; b++){ if(argv[a][b] == ','){ playtracks[num] = 1; num = 0; continue; } num *= 10; num += argv[a][b] - '0'; } playtracks[num] = 1; break; case 's': a++; if(a == argc){ printf("%s: option %s requires an argument\n", argv[0], argv[a-1]); return 1; } tempoMultiplier = atof(argv[a]); break; case 'i': // FIXME displayinfo = 1; // tracks, guesstimated length break; // -o loop to from // -b begin at // -e end at case 'l': tracing = 1; break; case 'n': play = 0; break; case 'k': skip = 1; break; case 'c': channelMask = 0; // channels num = 0; b = 0; a++; if(a == argc){ printf("%s: option %s requires an argument\n", argv[0], argv[a-1]); return 1; } for(b = 0; argv[a][b]; b++){ if(argv[a][b] == ','){ channelMask |= (1 << num); num = 0; continue; } num *= 10; num += argv[a][b] - '0'; } channelMask |= (1 << num); break; case 'r': a++; if(a == argc){ printf("%s: option %s requires an argument\n", argv[0], argv[a-1]); return 1; } transpose = atoi(argv[a]); break; case 'v': verbose = 1; break; case 'h': printf("Usage: %s [options...] file\n", argv[0]); printf(" Options:\n"); printf(" -t comma separated list of tracks to play (default: all)\n"); printf(" -s tempo (speed) multiplier (default: 1.0)\n"); printf(" -i file info (track list)\n"); printf(" -l list notes as they are played (in the format totablature expects)\n"); printf(" -n no sound; don't actually play the midi\n"); printf(" -c comma separated list of channels to play (default: all)\n"); printf(" -r transpose notes by amount (default: 0)\n"); printf(" -k skip long sections of silence (good for playing single tracks)\n"); printf(" -v verbose; list all events except note on / note off\n"); printf(" -h shows this help screen\n"); return 0; break; default: printf("%s: unknown command line option: %s\n", argv[0], argv[1]); return 1; } else filename = argv[a]; } if(filename == null){ printf("%s: no file given. Try %s -h for help.\n", argv[0], argv[0]); return 1; } loadMidi(&mid, filename); if(mid == null){ printf("%s: unable to read file %s\n", argv[0], filename); return 1; } if(displayinfo){ int len = getMidiLength(mid); printf("File: %s\n", filename); printf("Ticks per quarter note: %d\n", mid.speed); printf("Initial tempo: %d\n", getMidiTempo(mid)); printf("Length: %d:%d\n", len / 60, len%60); printf("Tracks:\n"); for(a = 0; a < mid.numTracks; a++){ c[0] = getTrackNameChunk(mid, a); if(c[0] != null){ printf("%d: ", a); for(b = 0; b < c[0].length; b++) fputc(c[0].data[b], stdout); printf("\n"); } } freeMidi(&mid); return 0; } return 0; } }