File size: 8,051 Bytes
af6912c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
//    abc_midi_renderer.js: Create the actual format for the midi.

var centsToFactor = require("./cents-to-factor");
var rendererFactory;

(function() {
	"use strict";
	function setAttributes(elm, attrs) {
		for (var attr in attrs)
			if (attrs.hasOwnProperty(attr))
				elm.setAttribute(attr, attrs[attr]);
		return elm;
	}

	function Midi() {
		this.trackstrings = "";
		this.trackcount = 0;
		this.noteOnAndChannel = "%90";
		this.noteOffAndChannel = "%80";
	}

	Midi.prototype.setTempo = function(qpm) {
		if (this.trackcount === 0) {
			this.startTrack();
			this.track += "%00%FF%51%03" + toHex(Math.round(60000000 / qpm), 6);
			this.endTrack();
		}
	};

	Midi.prototype.setGlobalInfo = function(qpm, name, key, time) {
		if (this.trackcount === 0) {
			this.startTrack();
			var divisions = Math.round(60000000 / qpm);
			// Add the tempo
			this.track += "%00%FF%51%03" + toHex(divisions, 6);

			if (key)
				this.track += keySignature(key);
			if (time)
				this.track += timeSignature(time);
			if (name) {
				this.track += encodeString(name, "%01");
			}
			this.endTrack();
		}
	};

	Midi.prototype.startTrack = function() {
		this.noteWarped = {};
		this.track = "";
		this.trackName = "";
		this.trackInstrument = "";
		this.silencelength = 0;
		this.trackcount++;
		if (this.instrument) {
			this.setInstrument(this.instrument);
		}
	};

	Midi.prototype.endTrack = function() {
		this.track = this.trackName + this.trackInstrument + this.track;
		var tracklength = toHex(this.track.length / 3 + 4, 8);
		this.track = "MTrk" + tracklength + // track header
			this.track +
			'%00%FF%2F%00'; // track end
		this.trackstrings += this.track;
	};

	Midi.prototype.setText = function(type, text) {
		// MIDI defines the following types of events:
		//FF 01 len text Text Event
		//FF 02 len text Copyright Notice
		//FF 03 len text Sequence/Track Name
		//FF 04 len text Instrument Name
		//FF 05 len text Lyric
		//FF 06 len text Marker
		//FF 07 len text Cue Point
		switch(type) {
			case 'name':
				this.trackName = encodeString(text, "%03");
				break;
		}
	};

	Midi.prototype.setInstrument = function(number) {
		this.trackInstrument = "%00%C0" + toHex(number, 2);
		this.instrument = number;
	};

	Midi.prototype.setChannel = function(number, pan) {
		this.channel = number;
		var ccPrefix = "%00%B" + this.channel.toString(16);
		// Reset midi, in case it was set previously.
		this.track += ccPrefix + "%79%00"; // Reset All Controllers
		this.track += ccPrefix + "%40%00"; // Damper pedal
		this.track += ccPrefix + "%5B%30"; // Effect 1 Depth (reverb)
		// Translate pan as -1 to 1 to 0 to 127
		if (!pan)
			pan = 0;
		pan = Math.round((pan + 1) * 64);
		this.track += ccPrefix + "%0A" + toHex(pan, 2); // Pan
		this.track += ccPrefix + "%07%64"; // Channel Volume

		this.noteOnAndChannel = "%9" + this.channel.toString(16);
		this.noteOffAndChannel = "%8" + this.channel.toString(16);
	};

	var HALF_STEP = 4096; // For the pitch wheel - (i.e. the distance from C to C#)
	Midi.prototype.startNote = function(pitch, loudness, cents) {
		this.track += toDurationHex(this.silencelength); // only need to shift by amount of silence (if there is any)
		this.silencelength = 0;
		if (cents) {
			// the pitch is altered so send a midi pitch wheel event
			this.track += "%e" + this.channel.toString(16);
			var bend = Math.round(centsToFactor(cents)*HALF_STEP);
			this.track += to7BitHex(0x2000 + bend);
			this.track += toDurationHex(0); // this all happens at once so there is a zero length here
			this.noteWarped[pitch] = true;
		}
		this.track += this.noteOnAndChannel;
		this.track += "%" + pitch.toString(16) + toHex(loudness, 2); //note
	};

	Midi.prototype.endNote = function(pitch) {
		this.track += toDurationHex(this.silencelength); // only need to shift by amount of silence (if there is any)
		this.silencelength = 0;
		if (this.noteWarped[pitch]) {
			// the pitch was altered so alter it back.
			this.track += "%e" + this.channel.toString(16);
			this.track += to7BitHex(0x2000);
			this.track += toDurationHex(0); // this all happens at once so there is a zero length here
			this.noteWarped[pitch] = false;
		}
		this.track += this.noteOffAndChannel;
		this.track += "%" + pitch.toString(16) + "%00";//end note
	};

	Midi.prototype.addRest = function(length) {
		this.silencelength += length;
		if (this.silencelength < 0)
			this.silencelength = 0;
	};

	Midi.prototype.getData = function() {
		return "data:audio/midi," +
			"MThd%00%00%00%06%00%01" + toHex(this.trackcount, 4) + "%01%e0" + // header
			this.trackstrings;
	};

	Midi.prototype.embed = function(parent, noplayer) {

		var data = this.getData();

		var link = setAttributes(document.createElement('a'), {
			href: data
		});
		link.innerHTML = "download midi";
		parent.insertBefore(link, parent.firstChild);

		if (noplayer) return;

		var embed = setAttributes(document.createElement('embed'), {
			src: data,
			type: 'video/quicktime',
			controller: 'true',
			autoplay: 'false',
			loop: 'false',
			enablejavascript: 'true',
			style: 'display:block; height: 20px;'
		});
		parent.insertBefore(embed, parent.firstChild);
	};

	function encodeString(str, cmdType) {
		// If there are multi-byte chars, we don't know how long the string will be until we create it.
		var nameArray = "";
		for (var i = 0; i < str.length; i++)
			nameArray += toHex(str.charCodeAt(i), 2);
		return "%00%FF" + cmdType + toHex(nameArray.length/3, 2) + nameArray; // Each byte is represented by three chars "%XX", so divide by 3 to get the length.
	}

	function keySignature(key) {
		//00 FF 5902 03 00 - key signature
		if (!key || !key.accidentals)
			return "";
		var hex = "%00%FF%59%02";
		var sharpCount = 0;
		var flatCount = 256;
		for (var i = 0; i < key.accidentals.length; i++) {
			if (key.accidentals[i].acc === "sharp") sharpCount++;
			else if (key.accidentals[i].acc === "flat") flatCount--;
		}
		var sig = flatCount !== 256 ? toHex(flatCount, 2) : toHex(sharpCount, 2);
		var mode = (key.mode === "m") ? "%01" : "%00";
		return hex + sig + mode;
	}

	function timeSignature(time) {
		//00 FF 58 04 04 02 30 08 - time signature
		var hex = "%00%FF%58%04" + toHex(time.num,2);
		var dens = { 1: 0, 2: 1, 4: 2, 8: 3, 16: 4, 32: 5 };
		var den = dens[time.den];
		if (!den)
			return ""; // the denominator is not supported, so just don't include this.
		hex += toHex(den, 2);

		var clocks;
		switch (time.num+"/"+time.den) {
			case "2/4":
			case "3/4":
			case "4/4":
			case "5/4":
				clocks = 24;
				break;
			case "6/4":
				clocks = 72;
				break;
			case "2/2":
			case "3/2":
			case "4/2":
				clocks = 48;
				break;
			case "3/8":
			case "6/8":
			case "9/8":
			case "12/8":
				clocks = 36;
				break;
		}
		if (!clocks)
			return ""; // time sig is not supported.
		hex += toHex(clocks, 2);
		return hex + "%08";
	}

	// s is assumed to be of even length
	function encodeHex(s) {
		var ret = "";
		for (var i = 0; i < s.length; i += 2) {
			ret += "%";
			ret += s.substr(i, 2);
		}
		return ret;
	}

	function toHex(n, padding) {
		var s = n.toString(16);
		s = s.split(".")[0];
		while (s.length < padding) {
			s = "0" + s;
		}
		if (s.length > padding)
			s = s.substring(0,padding)
		return encodeHex(s);
	}

	function to7BitHex(n) {
		// this takes a number and shifts all digits from the 7th one to the left.
		n = Math.round(n);
		var lower = n % 128;
		var higher = n - lower;
		return toHex(higher*2+lower, 4);
	}

	function toDurationHex(n) {
		var res = 0;
		var a = [];

		// cut up into 7 bit chunks;
		n = Math.round(n);
		while (n !== 0) {
			a.push(n & 0x7F);
			n = n >> 7;
		}

		// join the 7 bit chunks together, all but last chunk get leading 1
		for (var i = a.length - 1; i >= 0; i--) {
			res = res << 8;
			var bits = a[i];
			if (i !== 0) {
				bits = bits | 0x80;
			}
			res = res | bits;
		}

		var padding = res.toString(16).length;
		padding += padding % 2;

		return toHex(res, padding);
	}

	rendererFactory = function() {
		return new Midi();
	};
})();

module.exports = rendererFactory;