forked from XAYRGA/JAIMaker
-
Notifications
You must be signed in to change notification settings - Fork 0
/
MidiToBMS.cs
336 lines (295 loc) · 14.2 KB
/
MidiToBMS.cs
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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MidiSharp;
using System.IO;
using Be.IO;
using JaiSeqX.JAI.Seq;
namespace JaiMaker
{
static class MidiToBMS
{
static MemoryStream[] JaiTracks;
static int[] DeltaEnds;
static int[] TotalTrackDeltas;
static int[] TrackAddresses;
static int[] TrackLoops;
static bool at_least_one_loop;
static MemoryStream JaiTrackFinal;
public static void doToBMS(MidiSequence wtf, string filename)
{
var largest_delta = 0;
JaiTrackFinal = new MemoryStream(); // Buffer for JaiSeq track
var JaiWriter = new BeBinaryWriter(JaiTrackFinal); // Writer into JaiSeq buffer
DeltaEnds = new int[wtf.Tracks.Count]; // The deltas that will be smashed onto the end of every track.
TotalTrackDeltas = new int[wtf.Tracks.Count]; // The total delta sizes for each track
TrackAddresses = new int[wtf.Tracks.Count]; // The addresses that each track is at.
TrackLoops = new int[wtf.Tracks.Count];
var rootTrack_OpenTrackPos = JaiWriter.BaseStream.Position; // Store the position where our Open track commands are.
for (int i = 0; i < wtf.Tracks.Count; i++)
{
JaiWriter.Write((byte)JaiSeqEvent.OPEN_TRACK); // Write dummy open track commands
JaiWriter.Write((byte)i); // Index
writeInt24BE(JaiWriter, 0); // 0-size pointer.
}
JaiWriter.Write((byte)JaiSeqEvent.TIME_BASE); // Tell JAI to set the timebase
JaiWriter.Write((short)wtf.TicksPerBeatOrFrame); // Write timebase value
JaiWriter.Write((byte)JaiSeqEvent.TEMPO); // Tell JAI to write the tempo
JaiWriter.Write((ushort)Root.Tempo); // Write the tempo value.
writePrint(JaiWriter, @"Generated by Xayrga's JAIMaker.\n");
writePrint(JaiWriter, @"JAIMakerInitTrack: ");
JaiWriter.Write((byte)JaiSeqEvent.WAIT_8); // Wait
JaiWriter.Write((byte)2); // for 0xFFFA ticks
writePrint(JaiWriter, @" || DONE. \n");
var rootTrack_JumpPos = JaiWriter.BaseStream.Position; // Store the position where we want to jump back to
JaiWriter.Write((byte)JaiSeqEvent.WAIT_16); // Wait
JaiWriter.Write((ushort)0xFFFA); // for 0xFFFA ticks
JaiWriter.Write((byte)JaiSeqEvent.JUMP_COND); // Jump to position if
JaiWriter.Write((byte)0); // (always)
writeInt24BE(JaiWriter, (int)rootTrack_JumpPos); // int24 position.
for (int tridx = 0; tridx < wtf.Tracks.Count; tridx++)
{
var total_trk_delta = 0; // Total delta for current track
var CTrk = wtf.Tracks[tridx]; // Current track object
for (int evntid = 0; evntid < CTrk.Events.Count; evntid++) // Iterate through events
{
var CEvent = CTrk.Events[evntid]; // Current event object
total_trk_delta += (int)CEvent.DeltaTime; // Add the event to the total delta for our track
}
TotalTrackDeltas[tridx] = total_trk_delta; // Once we've iterated through all events we store the total track delta.
if (total_trk_delta > largest_delta) // We want to know what our highest delta is so we can make all the tracks end at the same time.
{
largest_delta = total_trk_delta; // So we should store it if it's greater than the last
}
}
for (int trk = 0; trk < wtf.Tracks.Count; trk++)
{
DeltaEnds[trk] = largest_delta - TotalTrackDeltas[trk]; // Now we know when all the tracks will end at the same time, so we want to sandwhich that onto the end of the track.
}
JaiWriter.Write((byte)JaiSeqEvent.FIN); // Write the finisher for the track, not because it's required but just becase it's standard.
for (int TrackID = 0; TrackID < wtf.Tracks.Count; TrackID++)
{
var MidTrack = wtf.Tracks[TrackID];
TrackAddresses[TrackID] = (int)JaiWriter.BaseStream.Position; // Store track position
JaiWriter.Write((byte)0xA4);
JaiWriter.Write((byte)0x20);
JaiWriter.Write((byte)Root.instrumentBanks[TrackID]);
JaiWriter.Write((byte)0xA4);
JaiWriter.Write((byte)0x21);
JaiWriter.Write((byte)Root.programs[TrackID]);
//Write SyncCPU 200 at the very beginning of the song on every track
JaiWriter.Write((byte)0xE7);
JaiWriter.Write((short)200);
writePrint(JaiWriter, @", T" + TrackID);
Stack<byte> voiceStack = new Stack<byte>(8); // JAISeq has 8 voices per track.
Queue<byte> notehistory = new Queue<byte>(8);
for (byte v = 7; v > 0; v--) // Push all of them to be ready.
{
Console.WriteLine("PushVoice {0}", v);
voiceStack.Push(v); // Push to stack.
}
byte[] voiceMap = new byte[1024]; // Keeps track of what MIDI notes are currently playing on what voices.
for (int ev = 0; ev < MidTrack.Events.Count; ev++)
{
var cevent = MidTrack.Events[ev];
if (cevent.DeltaTime > 0) // Does the event have any delta?
{
writeDelta(JaiWriter, (int)cevent.DeltaTime);
}
if (cevent is MidiSharp.Events.Meta.Text.CuePointTextMetaMidiEvent)
{
var mevent = (MidiSharp.Events.Meta.Text.CuePointTextMetaMidiEvent)cevent;
Console.WriteLine("YEET {0} - {1} ", TrackID, mevent.Text);
if (mevent.Text=="JLOOP")
{
TrackLoops[TrackID] = (int)JaiWriter.BaseStream.Position;
at_least_one_loop = true;
}
}
else if (cevent is MidiSharp.Events.Voice.Note.OnNoteVoiceMidiEvent)
{
var mevent = (MidiSharp.Events.Voice.Note.OnNoteVoiceMidiEvent)cevent;
if (voiceMap[mevent.Note] != 0) // This voice is already in use and hasnt been told to stop?
{
var stopVoice = voiceMap[mevent.Note]; // grab the voice id
{
voiceStack.Push(stopVoice);
JaiWriter.Write((byte)(0x80 + stopVoice));
}
}
if (voiceStack.Count < 1) // if theres no voice available
{
for (int i = 0; i < voiceMap.Length; i++) // Iterate through voices table
{
if (voiceMap[i] > 0) // if a voice is greater than 0 (used)
{
voiceStack.Push(voiceMap[i]); // Dealloc it (push to voice table)
voiceMap[i] = 0; // Set it to 0, it has no pitch, it's been deallocd
}
}
}
if (voiceStack.Count > 0)
{
JaiWriter.Write(mevent.Note); // JAI Note on events are literally just midi notes.
var useVoice = voiceStack.Pop(); // Grab next available voice.
voiceMap[mevent.Note] = useVoice; // Map it to the pitch
JaiWriter.Write(useVoice); // write which JAIVoice it will be using
JaiWriter.Write(mevent.Velocity); // Write its velocity.
}
else
{
Console.WriteLine("Too many voices {0}", TrackID);
}
}
else if (cevent is MidiSharp.Events.Voice.Note.OffNoteVoiceMidiEvent)
{
var mevent = (MidiSharp.Events.Voice.Note.OffNoteVoiceMidiEvent)cevent;
var stopVoice = voiceMap[mevent.Note];
if (stopVoice == 0)
{
Console.WriteLine("VOICE LEAK {0}", TrackID);
}
else
{
voiceMap[mevent.Note] = 0;
voiceStack.Push(stopVoice);
//Console.WriteLine("Stop voice {0}",stopVoice);
JaiWriter.Write((byte)(0x80 + stopVoice));
}
}
}
// Done with parsting track events.
if (DeltaEnds[TrackID] > 0)
{
writeDelta(JaiWriter, DeltaEnds[TrackID]); // Write finishing delta to make sure all tracks end at the same point.
}
for (int i=0; i < voiceMap.Length; i++)
{
if (voiceMap[i] > 0)
{
JaiWriter.Write((byte)(0x80 + voiceMap[i])); // stop all notes before song end.
Console.WriteLine("=== STOP VOICE END SONG {0}", voiceMap[i]);
}
}
if (TrackLoops[TrackID] > 0)
{
JaiWriter.Write((byte)JaiSeqEvent.JUMP_COND); // Jump to position if
JaiWriter.Write((byte)0); // (always)
writeInt24BE(JaiWriter, TrackLoops[TrackID]); // int24 position.
}
JaiWriter.Write((byte)JaiSeqEvent.FIN);
while (JaiWriter.BaseStream.Position % 32 > 0)
{
JaiWriter.Write((byte)0);
}
}
// Done with parsing tracks.
JaiWriter.BaseStream.Position = rootTrack_OpenTrackPos; // We have to update the track opening points that we had before.
for (int i = 0; i < wtf.Tracks.Count; i++)
{
JaiWriter.Write((byte)JaiSeqEvent.OPEN_TRACK); // Write open track command
JaiWriter.Write((byte)i); // Index
writeInt24BE(JaiWriter, TrackAddresses[i]);
}
JaiWriter.Flush();
File.WriteAllBytes(filename, ReadToEnd(JaiTrackFinal));
}
public static void writeDelta(BeBinaryWriter JaiWriter, int delta)
{
if (delta < 0xFF) // 8-bit wait
{
JaiWriter.Write((byte)JaiSeqEvent.WAIT_8); // 8 bit wait command
JaiWriter.Write((byte)delta); // write delta
}
else if (delta < 0xFFFF) // 16 bit wait
{
JaiWriter.Write((byte)JaiSeqEvent.WAIT_16);
JaiWriter.Write((ushort)delta);
}
else // dont feel like writing VLQ timing, so i'll just spam u16 waits :V
{ // VLQ wait.
var total = delta;
while (total > 0xFFFA)
{
total -= 0xFFFA;
JaiWriter.Write((byte)JaiSeqEvent.WAIT_16);
JaiWriter.Write((ushort)0xFFFA);
}
if (total > 0)
{
JaiWriter.Write((byte)JaiSeqEvent.WAIT_16);
JaiWriter.Write((ushort)total);
}
}
}
public static void writePrint(BeBinaryWriter bw,string data)
{
/*
var b = Encoding.ASCII.GetBytes(data);
bw.Write((byte)0xFB); // PrintF
for (int i = 0; i < data.Length; i ++ )
{
bw.Write(data[i]);
}
bw.Write((byte)0x00);
// bw.Write((byte)0x00);
*/
}
public static void writeInt24BE(BeBinaryWriter bw,int ta)
{
var b1 = (ta) & 0xFF;
var b2 = (ta >> 8) & 0xFF;
var b3 = (ta >> 16) & 0xFF;
bw.Write((byte)b3);
bw.Write((byte)b2);
bw.Write((byte)b1);
}
public static byte[] ReadToEnd(System.IO.Stream stream)
{
long originalPosition = 0;
if (stream.CanSeek)
{
originalPosition = stream.Position;
stream.Position = 0;
}
try
{
byte[] readBuffer = new byte[4096];
int totalBytesRead = 0;
int bytesRead;
while ((bytesRead = stream.Read(readBuffer, totalBytesRead, readBuffer.Length - totalBytesRead)) > 0)
{
totalBytesRead += bytesRead;
if (totalBytesRead == readBuffer.Length)
{
int nextByte = stream.ReadByte();
if (nextByte != -1)
{
byte[] temp = new byte[readBuffer.Length * 2];
Buffer.BlockCopy(readBuffer, 0, temp, 0, readBuffer.Length);
Buffer.SetByte(temp, totalBytesRead, (byte)nextByte);
readBuffer = temp;
totalBytesRead++;
}
}
}
byte[] buffer = readBuffer;
if (readBuffer.Length != totalBytesRead)
{
buffer = new byte[totalBytesRead];
Buffer.BlockCopy(readBuffer, 0, buffer, 0, totalBytesRead);
}
return buffer;
}
finally
{
if (stream.CanSeek)
{
stream.Position = originalPosition;
}
}
}
}
}