-
Notifications
You must be signed in to change notification settings - Fork 21
/
midifile.c
254 lines (203 loc) · 5.64 KB
/
midifile.c
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
/*
* gbsplay is a Gameboy sound player
*
* common code of MIDI output plugins
*
* 2006-2024 (C) by Christian Garbs <[email protected]>
* Vegard Nossum
*
* Licensed under GNU GPL v1 or, at your option, any later version.
*/
#include <stdlib.h>
#include "common.h"
#include "util.h"
#include "filewriter.h"
#include "plugout.h"
#include "midifile.h"
/*
* The hardware emulation runs at 4194304 Hz and provides an exact
* cycle counter. Because ~4 MHz are outside of the possible MIDI
* time resolution, we introduce an arbitrary lower resolution of 256
* Hz by shifting the cycle counter 14 bits to the right.
*
* This lower resolution (let's call it a "tick" because that's what
* our target on the MIDI side is called) is
*
* 1 tick = 4194304 Hz / 2^14 == 1/256 Hz == 3906.25 us
*/
static const uint8_t CYCLE_RESOLUTION_BIT_SHIFT = 14;
/*
* Now we need to make a single MIDI tick match the 3906.25 us from above.
*
* In a MIDI file, the Time Division in the header chunk is used to
* define the temporal resolution of a MIDI file. It is a 16 bit
* value with two possible calculation modes:
*
* Top bit == 0 selects PPQ (Pulses per quarter note)
* Top bit == 1 selects Frames per Second
*
* When using PPQ (pulses per quarter note) mode, the actual length of
* a single MIDI tick is calculated as:
*
* 1 tick = TEMPO / TIME_DIVISION
*
* TEMPO is measured in us/beat (1 beat == 1 quarter note)
* and can be set by the tempo meta command.
*
* A nicely matching pair of values would be:
*
* TEMPO = 500000 us / beat
* TIME_DIVISION = 128 / beat
*
* so that
*
* 1 tick = 500000 us / 128 == 3906.25 us == 2^14 hardware cycles
*
* exactly matches our reduced resolution from before.
*
* A TEMPO of 500000 us/beat (120 bpm) just happens to be the default
* value for MIDI files, so we don't bother to actually set the MIDI
* tempo when writing a track.
*/
static const uint32_t TIME_DIVISION =
(0 << 16) // select PPQ mode in highest bit
+ 128; // actual value as calculated above
static const long TRACK_LENGTH_OFFSET = 18;
static const long TRACK_START_OFFSET = 22;
static long mute[4] = {0, 0, 0, 0};
static FILE* file = NULL;
static cycles_t cycles_prev = 0;
int note[4];
int midi_file_error() {
return ferror(file);
}
int midi_file_is_closed() {
return file == NULL;
}
static int midi_file_close() {
int result;
if (midi_file_is_closed())
return -1;
result = fclose(file);
file = NULL;
return result;
}
void midi_update_mute(const struct gbs_channel_status status[]) {
for (int chan = 0; chan < 4; chan++)
mute[chan] = status[chan].mute;
}
static void midi_write_varlen(uint32_t value)
{
/* Big endian. Highest allowed value is 0x0fffffff */
for (int shift = 21; shift > 0; shift -= 7) {
uint8_t v = (value >> shift) & 0x7f;
if (v) {
fputc(v | 0x80, file);
}
}
fputc(value & 0x7f, file);
}
static void midi_write_event(cycles_t cycles, const uint8_t *data, unsigned int length)
{
cycles_t cycles_delta = cycles - cycles_prev;
unsigned long timestamp_delta = (cycles_delta) >> CYCLE_RESOLUTION_BIT_SHIFT;
midi_write_varlen(timestamp_delta);
fwrite(data, length, 1, file);
// only advance as far as the timestamp resolution allows, so we don't
// accumulate errors from repeatedly throwing away the lower bits
cycles_prev += timestamp_delta << CYCLE_RESOLUTION_BIT_SHIFT;
}
static int midi_open_track(int subsong)
{
if ((file = file_open("mid", subsong)) == NULL)
goto error;
/* File header */
fpack(file, ">{MThd}dwww",
6, /* header length */
0, /* format */
1, /* tracks */
/* TODO: Do some real calculation instead of this magic number :-) */
TIME_DIVISION /* division */);
/* Track header */
fpack(file, ">{MTrk}d", 0 /* length placeholder */);
if (ferror(file))
goto error;
return 0;
error:
if (file != NULL)
midi_file_close();
return 1;
}
static int midi_close_track()
{
long track_end_offset;
uint32_t track_length;
uint8_t event[] = { 0xff, 0x2f, 0x00 }; /* End of track */
midi_write_event(cycles_prev, event, sizeof(event));
/* Update length in header */
track_end_offset = ftell(file);
if (track_end_offset < 0 || track_end_offset > 0xffffffff)
goto error;
track_length = track_end_offset - TRACK_START_OFFSET;
fpackat(file, TRACK_LENGTH_OFFSET, ">d", track_length);
/* Close the file */
if (midi_file_close() == -1)
return 1;
return 0;
error:
if (file != NULL)
midi_file_close();
return 1;
}
void midi_note_on(cycles_t cycles, int channel, int new_note, int velocity)
{
uint8_t event[] = { 0x90 | channel, new_note, velocity };
if (mute[channel])
return;
midi_write_event(cycles, event, sizeof(event));
note[channel] = new_note;
}
void midi_note_off(cycles_t cycles, int channel)
{
uint8_t event[] = { 0x80 | channel, note[channel], 0 };
if (!note[channel])
return;
midi_write_event(cycles, event, sizeof(event));
note[channel] = 0;
}
void midi_pan(cycles_t cycles, int channel, int pan)
{
uint8_t event[] = { 0xb0 | channel, 0x0a, pan };
if (mute[channel])
return;
midi_write_event(cycles, event, sizeof(event));
}
long midi_open(enum plugout_endian *endian, long rate, long *buffer_bytes, const struct plugout_metadata metadata)
{
UNUSED(endian);
UNUSED(rate);
UNUSED(buffer_bytes);
UNUSED(metadata);
return 0;
}
int midi_skip(int subsong)
{
int channel;
if (!midi_file_is_closed()) {
if (midi_close_track())
return 1;
}
cycles_prev = 0;
for (channel = 0; channel < 4; channel++)
note[channel] = 0;
return midi_open_track(subsong);
}
void midi_close(void)
{
int channel;
if (midi_file_is_closed())
return;
for (channel = 0; channel < 4; channel++)
midi_note_off(cycles_prev + 1, channel);
midi_close_track();
}