arsd/midi.d

1274 lines
30 KiB
D

/**
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;
/+
So the midi ticks are defined in terms of per quarter note so that's good stuff.
If you're reading live though you have milliseconds, and probably want to round them
off a little to fit the beat.
+/
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.
+/
const(PlayStreamEvent)[] playbackStream() {
PlayStreamEvent[] stream;
size_t size;
foreach(track; tracks)
size += track.events.length;
stream.reserve(size);
Duration position;
static struct NoteOnInfo {
PlayStreamEvent* event;
int turnedOnTicks;
Duration turnedOnPosition;
}
NoteOnInfo[] noteOnInfo = new NoteOnInfo[](128 * 16);
scope(exit) noteOnInfo = null;
static struct LastNoteInfo {
PlayStreamEvent*[6] event; // in case there's a chord
int eventCount;
int turnedOnTicks;
}
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
void recordOff(scope NoteOnInfo* noi, int midiClockPosition) {
noi.event.noteOnDuration = position - noi.turnedOnPosition;
noi.event = null;
}
// FIXME: what about rests?
foreach(item; flattenedTrackStream) {
position += item.wait;
stream ~= item;
if(item.event.event == MIDI_EVENT_NOTE_ON) {
if(item.event.data2 == 0)
goto off;
auto ptr = &stream[$-1];
auto noi = &noteOnInfo[(item.event.channel & 0x0f) * 128 + (item.event.data1 & 0x7f)];
if(noi.event) {
recordOff(noi, item.midiClockPosition);
}
noi.event = ptr;
noi.turnedOnTicks = item.midiClockPosition;
noi.turnedOnPosition = position;
auto lni = &lastNoteInfo[(item.event.channel & 0x0f)];
if(lni.eventCount) {
if(item.midiClockPosition == lni.turnedOnTicks) {
if(lni.eventCount == lni.event.length)
goto maxedOut;
lni.event[lni.eventCount++] = ptr;
} else {
maxedOut:
foreach(ref e; lni.event[0 .. lni.eventCount])
e.midiTicksToNextNoteOnChannel = item.midiClockPosition - lni.turnedOnTicks;
goto frist;
}
} else {
frist:
lni.event[0] = ptr;
lni.eventCount = 1;
lni.turnedOnTicks = item.midiClockPosition;
}
} else if(item.event.event == MIDI_EVENT_NOTE_OFF) {
off:
auto noi = &noteOnInfo[(item.event.channel & 0x0f) * 128 + (item.event.data1 & 0x7f)];
if(noi.event) {
recordOff(noi, item.midiClockPosition);
}
}
}
return stream;
}
/++
Returns a forward range for playback or analysis that flattens the midi
tracks into a single stream. 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 iteration
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.
+/
FlattenedTrackStream flattenedTrackStream() {
return FlattenedTrackStream(this);
}
}
static struct PlayStreamEvent {
/// This is how long you wait until triggering this event.
/// Note it may be zero.
Duration wait;
/// And this is the midi event message.
MidiEvent event;
string toString() const {
return event.toString();
}
/// informational. May be null if the stream didn't come from a file or tracks.
MidiFile file;
/// ditto
MidiTrack track;
/++
Gives the position ot the global midi clock for this event. The `event.deltaTime`
is in units of the midi clock, but the actual event has the clock per-track whereas
this value is global, meaning it might not be the sum of event.deltaTime to this point.
(It should add up if you only sum ones with the same [track] though.
The midi clock is used in conjunction with the [MidiFile.timing] and current tempo
state to determine a real time wait value, which you can find in the [wait] member.
This position is probably less useful than the running sum of [wait]s, but is provided
just in case it is useful to you.
+/
int midiClockPosition;
/++
The duration between this non-zero velocity note on and its associated note off.
Will be zero if this isn't actually a note on, the input stream was not seekable (e.g.
a real time recording), or if a note off was not found ahead in the stream.
It is basically how long the pianist held down the key.
Be aware that that the note on to note off is not necessarily associated with the
note you'd see on sheet music. It is more about the time the sound actually rings,
but it may not exactly be that either due to the time it takes for the note to
fade out.
+/
Duration noteOnDuration;
/++
This is the count of midi clock ticks after this non-zero velocity note on event (if
it is not one of those, this value will be zero) and the next note that will be sounded
on its same channel.
While rests may throw this off, this number is the most help in this struct for determining
the note length you'd put on sheet music. Divide it by [MidiFile.timing] to get the number
of midi quarter notes, which is directly correlated to the musical beat.
Will be zero if this isn't actually a note on, the input stream was not seekable (e.g.
a real time recording where the next note hasn't been struck yet), or if a note off was
not found ahead in the stream.
+/
int midiTicksToNextNoteOnChannel;
// 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
}
static immutable(PlayStreamEvent)[] longWait = [{wait: 1.weeks, event: {status: 0xff, data1: 0x01, meta: null}}];
struct FlattenedTrackStream {
FlattenedTrackStream 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; // microseconds per quarter note
popFront();
}
//@nogc:
int midiClock;
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 = PlayStreamEvent(0.seconds, f, file, tp.track, midiClock);
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 = (cast(long) minWait * tempo / file.timing).usecs;
midiClock += minWait;
pending = PlayStreamEvent(time, trackPositions[minWaitTrack].remaining[0], file, trackPositions[minWaitTrack].track, midiClock);
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
PlayStreamEvent front() {
return pending;
}
private uint tempo;
private PlayStreamEvent 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.isMeta && newEvent.data1 == MetaEvent.Name)
name_ = cast(string) newEvent.meta.idup;
if(newEvent.status == 0xff && newEvent.data1 == MetaEvent.EndOfTrack) {
break;
}
events ~= newEvent;
}
//assert(begin - trackLength == buf.bytes.length);
}
/++
All the midi events found in the track.
+/
MidiEvent[] events;
/++
The name of the track, as found from metadata at load time.
This may change to scan events to see updates without the cache in the future.
+/
@property string name() {
return name_;
}
private string name_;
/++
This field is not used or stored in a midi file; it is just
a place to store some state in your player.
I use it to keep flags like if the track is currently enabled.
+/
int customPlayerInfo;
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); }
/++
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.
History:
Added January 2, 2022 (dub v10.5)
+/
static MidiEvent NoteOn(int channel, int note, int velocity) { return MidiEvent(0, (MIDI_EVENT_NOTE_ON << 4) | (channel & 0x0f), note & 0x7f, velocity & 0x7f); }
/// ditto
static MidiEvent NoteOff(int channel, int note, int velocity) { return MidiEvent(0, (MIDI_EVENT_NOTE_OFF << 4) | (channel & 0x0f), note & 0x7f, velocity & 0x7f); }
/+
// FIXME: this is actually a relatively complicated one i should fix, it combines bits... 8192 == 0.
// This is a bit of a magical function, it takes a signed bend between 0 and 81
static MidiEvent PitchBend(int channel, int bend) {
return MidiEvent(0, (MIDI_EVENT_PITCH_BEND << 4) | (channel & 0x0f), bend & 0x7f, bend & 0x7f);
}
+/
// this overload ok, it is what the thing actually tells. coarse == 64 means we're at neutral.
/// ditto
static MidiEvent PitchBend(int channel, int fine, int coarse) { return MidiEvent(0, (MIDI_EVENT_PITCH_BEND << 4) | (channel & 0x0f), fine & 0x7f, coarse & 0x7f); }
/// ditto
static MidiEvent NoteAftertouch(int channel, int note, int velocity) { return MidiEvent(0, (MIDI_EVENT_NOTE_AFTERTOUCH << 4) | (channel & 0x0f), note & 0x7f, velocity & 0x7f); }
// 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.
/// ditto
static MidiEvent Controller(int channel, int controller, int value) { return MidiEvent(0, (MIDI_EVENT_CONTROLLER << 4) | (channel & 0x0f), controller & 0x7f, value & 0x7f); }
// the two byte ones
/// ditto
static MidiEvent ProgramChange(int channel, int program) { return MidiEvent(0, (MIDI_EVENT_PROGRAM_CHANGE << 4) | (channel & 0x0f), program & 0x7f); }
/// ditto
static MidiEvent ChannelAftertouch(int channel, int param) { return MidiEvent(0, (MIDI_EVENT_CHANNEL_AFTERTOUCH << 4) | (channel & 0x0f), param & 0x7f); }
///
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);
s ~= " ";
s ~= tos(channel);
s ~= " ";
switch(event) {
case MIDI_EVENT_NOTE_OFF: s ~= "NOTE_OFF"; break;
case MIDI_EVENT_NOTE_ON: s ~= data2 ? "NOTE_ON" : "NOTE_ON_ZERO"; break;
case MIDI_EVENT_NOTE_AFTERTOUCH: s ~= "NOTE_AFTERTOUCH"; break;
case MIDI_EVENT_CONTROLLER: s ~= "CONTROLLER"; break;
case MIDI_EVENT_PROGRAM_CHANGE: s ~= "PROGRAM_CHANGE"; break;
case MIDI_EVENT_CHANNEL_AFTERTOUCH: s ~= "CHANNEL_AFTERTOUCH"; break;
case MIDI_EVENT_PITCH_BEND: s ~= "PITCH_BEND"; break;
default:
}
}
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;
}
}