-
Notifications
You must be signed in to change notification settings - Fork 0
/
MidiFile.java
285 lines (237 loc) · 7.28 KB
/
MidiFile.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
/*
A simple Java class that writes a MIDI file
(c)2011 Kevin Boone, all rights reserved
*/
package com.kevinboone.music;
import java.io.*;
import java.util.*;
public class MidiFile
{
// Note lengths
// We are working with 32 ticks to the crotchet. So
// all the other note lengths can be derived from this
// basic figure. Note that the longest note we can
// represent with this code is one tick short of a
// two semibreves (i.e., 8 crotchets)
static final int SEMIQUAVER = 4;
static final int QUAVER = 8;
static final int CROTCHET = 16;
static final int MINIM = 32;
static final int SEMIBREVE = 64;
// Standard MIDI file header, for one-track file
// 4D, 54... are just magic numbers to identify the
// headers
// Note that because we're only writing one track, we
// can for simplicity combine the file and track headers
static final int header[] = new int[]
{
0x4d, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06,
0x00, 0x00, // single-track format
0x00, 0x01, // one track
0x00, 0x10, // 16 ticks per quarter
0x4d, 0x54, 0x72, 0x6B
};
// Standard footer
static final int footer[] = new int[]
{
0x01, 0xFF, 0x2F, 0x00
};
// A MIDI event to set the tempo
static final int tempoEvent[] = new int[]
{
0x00, 0xFF, 0x51, 0x03,
0x0F, 0x42, 0x40 // Default 1 million usec per crotchet
};
// A MIDI event to set the key signature. This is irrelent to
// playback, but necessary for editing applications
static final int keySigEvent[] = new int[]
{
0x00, 0xFF, 0x59, 0x02,
0x00, // C
0x00 // major
};
// A MIDI event to set the time signature. This is irrelent to
// playback, but necessary for editing applications
static final int timeSigEvent[] = new int[]
{
0x00, 0xFF, 0x58, 0x04,
0x04, // numerator
0x02, // denominator (2==4, because it's a power of 2)
0x30, // ticks per click (not used)
0x08 // 32nd notes per crotchet
};
// The collection of events to play, in time order
protected Vector<int[]> playEvents;
/** Construct a new MidiFile with an empty playback event list */
public MidiFile()
{
playEvents = new Vector<int[]>();
}
/** Write the stored MIDI events to a file */
public void writeToFile (String filename)
throws IOException
{
FileOutputStream fos = new FileOutputStream (filename);
fos.write (intArrayToByteArray (header));
// Calculate the amount of track data
// _Do_ include the footer but _do not_ include the
// track header
int size = tempoEvent.length + keySigEvent.length + timeSigEvent.length
+ footer.length;
for (int i = 0; i < playEvents.size(); i++)
size += playEvents.elementAt(i).length;
// Write out the track data size in big-endian format
// Note that this math is only valid for up to 64k of data
// (but that's a lot of notes)
int high = size / 256;
int low = size - (high * 256);
fos.write ((byte) 0);
fos.write ((byte) 0);
fos.write ((byte) high);
fos.write ((byte) low);
// Write the standard metadata — tempo, etc
// At present, tempo is stuck at crotchet=60
fos.write (intArrayToByteArray (tempoEvent));
fos.write (intArrayToByteArray (keySigEvent));
fos.write (intArrayToByteArray (timeSigEvent));
// Write out the note, etc., events
for (int i = 0; i < playEvents.size(); i++)
{
fos.write (intArrayToByteArray (playEvents.elementAt(i)));
}
// Write the footer and close
fos.write (intArrayToByteArray (footer));
fos.close();
}
/** Convert an array of integers which are assumed to contain
unsigned bytes into an array of bytes */
protected static byte[] intArrayToByteArray (int[] ints)
{
int l = ints.length;
byte[] out = new byte[ints.length];
for (int i = 0; i < l; i++)
{
out[i] = (byte) ints[i];
}
return out;
}
/** Store a note-on event */
public void noteOn (int delta, int note, int velocity)
{
int[] data = new int[4];
data[0] = delta;
data[1] = 0x90;
data[2] = note;
data[3] = velocity;
playEvents.add (data);
}
/** Store a note-off event */
public void noteOff (int delta, int note)
{
int[] data = new int[4];
data[0] = delta;
data[1] = 0x80;
data[2] = note;
data[3] = 0;
playEvents.add (data);
}
/** Store a program-change event at current position */
public void progChange (int prog)
{
int[] data = new int[3];
data[0] = 0;
data[1] = 0xC0;
data[2] = prog;
playEvents.add (data);
}
/** Store a note-on event followed by a note-off event a note length
later. There is no delta value — the note is assumed to
follow the previous one with no gap. */
public void noteOnOffNow (int duration, int note, int velocity)
{
noteOn (0, note, velocity);
noteOff (duration, note);
}
public void noteSequenceFixedVelocity (int[] sequence, int velocity)
{
boolean lastWasRest = false;
int restDelta = 0;
for (int i = 0; i < sequence.length; i += 2)
{
int note = sequence[i];
int duration = sequence[i + 1];
if (note < 0)
{
// This is a rest
restDelta += duration;
lastWasRest = true;
}
else
{
// A note, not a rest
if (lastWasRest)
{
noteOn (restDelta, note, velocity);
noteOff (duration, note);
}
else
{
noteOn (0, note, velocity);
noteOff (duration, note);
}
restDelta = 0;
lastWasRest = false;
}
}
}
/** Test method — creates a file test1.mid when the class
is executed */
public static void main (String[] args)
throws Exception
{
MidiFile mf = new MidiFile();
// Test 1 — play a C major chord
// Turn on all three notes at start-of-track (delta=0)
mf.noteOn (0, 60, 127);
mf.noteOn (0, 64, 127);
mf.noteOn (0, 67, 127);
// Turn off all three notes after one minim.
// NOTE delta value is cumulative — only _one_ of
// these note-offs has a non-zero delta. The second and
// third events are relative to the first
mf.noteOff (MINIM, 60);
mf.noteOff (0, 64);
mf.noteOff (0, 67);
// Test 2 — play a scale using noteOnOffNow
// We don't need any delta values here, so long as one
// note comes straight after the previous one
mf.noteOnOffNow (QUAVER, 60, 127);
mf.noteOnOffNow (QUAVER, 62, 127);
mf.noteOnOffNow (QUAVER, 64, 127);
mf.noteOnOffNow (QUAVER, 65, 127);
mf.noteOnOffNow (QUAVER, 67, 127);
mf.noteOnOffNow (QUAVER, 69, 127);
mf.noteOnOffNow (QUAVER, 71, 127);
mf.noteOnOffNow (QUAVER, 72, 127);
// Test 3 — play a short tune using noteSequenceFixedVelocity
// Note the rest inserted with a note value of -1
int[] sequence = new int[]
{
60, QUAVER + SEMIQUAVER,
65, SEMIQUAVER,
70, CROTCHET + QUAVER,
69, QUAVER,
65, QUAVER / 3,
62, QUAVER / 3,
67, QUAVER / 3,
72, MINIM + QUAVER,
-1, SEMIQUAVER,
72, SEMIQUAVER,
76, MINIM,
};
// What the heck — use a different instrument for a change
mf.progChange (10);
mf.noteSequenceFixedVelocity (sequence, 127);
mf.writeToFile ("test1.mid");
}
}