File size: 17,306 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
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
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
//    wrap_lines.js: does line wrap on an already parsed tune.

function wrapLines(tune, lineBreaks, barNumbers) {
	if (!lineBreaks || tune.lines.length === 0)
		return;

	// tune.lines contains nested arrays: there is an array of lines (that's the part this function rewrites),
	// there is an array of staffs per line (for instance, piano will have 2, orchestra will have many)
	// there is an array of voices per staff (for instance, 4-part harmony might have bass and tenor on a single staff)
	var lines = tune.deline({lineBreaks: false});
	var linesBreakElements = findLineBreaks(lines, lineBreaks);
	//console.log(JSON.stringify(linesBreakElements))
	tune.lines = addLineBreaks(lines, linesBreakElements, barNumbers);
	tune.lineBreaks = linesBreakElements;
}

function addLineBreaks(lines, linesBreakElements, barNumbers) {
	// linesBreakElements is an array of all of the elements that break for a new line
	// The objects in the array look like:
	// {"ogLine":0,"line":0,"staff":0,"voice":0,"start":0, "end":21}
	// ogLine is the original line that it came from,
	// line is the target line.
	// then copy all the elements from start to end for the staff and voice specified.
	// If the item doesn't contain "staff" then it is a non music line and should just be copied.
	var outputLines = [];
	var lastKeySig = []; // This is per staff - if the key changed then this will be populated.
	var lastStem = [];
	var currentBarNumber = 1;
	for (var i = 0; i < linesBreakElements.length; i++) {
		var action = linesBreakElements[i];
		if (lines[action.ogLine].staff) {
			var inputStaff = lines[action.ogLine].staff[action.staff];
			if (!outputLines[action.line]) {
				outputLines[action.line] = {staff: []}
			}
			if (!outputLines[action.line].staff[action.staff]) {
				outputLines[action.line].staff[action.staff] = {voices: []};
				if (barNumbers !== undefined && action.staff === 0 && action.line > 0) {
					outputLines[action.line].staff[action.staff].barNumber = currentBarNumber;
				}
				var keys = Object.keys(inputStaff)
				for (var k = 0; k < keys.length; k++) {
					var skip = keys[k] === "voices";
					if (keys[k] === "meter" && action.line !== 0)
						skip = true;
					if (!skip)
						outputLines[action.line].staff[action.staff][keys[k]] = inputStaff[keys[k]];
				}
				if (lastKeySig[action.staff])
					outputLines[action.line].staff[action.staff].key = lastKeySig[action.staff];

			}
			if (!outputLines[action.line].staff[action.staff].voices[action.voice]) {
				outputLines[action.line].staff[action.staff].voices[action.voice] = [];
			}
			outputLines[action.line].staff[action.staff].voices[action.voice] =
				lines[action.ogLine].staff[action.staff].voices[action.voice].slice(action.start, action.end+1);
			if (lastStem[action.staff*10+action.voice])
				outputLines[action.line].staff[action.staff].voices[action.voice].unshift({el_type: "stem", direction: lastStem[action.staff*10+action.voice].direction})
			var currVoice = outputLines[action.line].staff[action.staff].voices[action.voice];
			for (var kk = currVoice.length-1; kk >= 0; kk--) {
				if (currVoice[kk].el_type === "key") {
					lastKeySig[action.staff] = {
						root: currVoice[kk].root,
						acc: currVoice[kk].acc,
						mode: currVoice[kk].mode,
						accidentals: currVoice[kk].accidentals.filter(function (acc) { return acc.acc !== 'natural' })
					};
					break;
				}
			}
			for (kk = currVoice.length-1; kk >= 0; kk--) {
				if (currVoice[kk].el_type === "stem") {
					lastStem[action.staff*10+action.voice] = {
						direction: currVoice[kk].direction,
					};
					break;
				}
			}
			if (barNumbers !== undefined && action.staff === 0 && action.voice === 0) {
				for (kk = 0; kk < currVoice.length; kk++) {
					if (currVoice[kk].el_type === 'bar') {
						currentBarNumber++
						if (kk === currVoice.length-1)
							delete currVoice[kk].barNumber
						else
							currVoice[kk].barNumber = currentBarNumber
					}
				}
			}
		} else {
			outputLines[action.line] = lines[action.ogLine];
		}
	}
	// There could be some missing info - if the tune passed in was incomplete or had different lengths for different voices or was missing a voice altogether - just fill in the gaps.
	for (var ii = 0; ii < outputLines.length; ii++) {
		if (outputLines[ii].staff) {
			outputLines[ii].staff = outputLines[ii].staff.filter(function (el) {
				return el != null;
			});
		}
	}
	return outputLines;
}


function findLineBreaks(lines, lineBreakArray) {
	// lineBreakArray is an array of all of the sections of the tune - often there will just be one
	// section unless there is a subtitle or other non-music lines. Each of the elements of
	// Each element of lineBreakArray is an array of the zero-based last measure of the line.
	var lineBreakIndexes = [];
	var lbai = 0;
	var lineCounter = 0;
	var outputLine = 0;
	for (var i = 0; i < lines.length; i++) {
		var line = lines[i];
		if (line.staff) {
			var lineStart = lineCounter;
			var lineBreaks = lineBreakArray[lbai];
			lbai++;
			for (var j = 0; j < line.staff.length; j++) {
				var staff = line.staff[j];
				for (var k = 0; k < staff.voices.length; k++) {
					outputLine = lineStart;
					var measureNumber = 0;
					var lbi = 0;
					var voice = staff.voices[k];
					var start = 0;
					for (var e = 0; e < voice.length; e++) {
						var el = voice[e];

						if (el.el_type === 'bar') {
							if (lineBreaks[lbi] === measureNumber) {
								lineBreakIndexes.push({ ogLine: i, line: outputLine, staff: j, voice: k, start: start, end: e})
								start = e + 1;
								outputLine++;
								lineCounter = Math.max(lineCounter, outputLine)
								lbi++;
							}
							measureNumber++;

						}
					}
					lineBreakIndexes.push({ ogLine: i, line: outputLine, staff: j, voice: k, start: start, end: voice.length})
					outputLine++;
					lineCounter = Math.max(lineCounter, outputLine)
				}
			}
		} else {
			lineBreakIndexes.push({ ogLine: i, line: outputLine })
			outputLine++;
			lineCounter = Math.max(lineCounter, outputLine)
		}
	}
	return lineBreakIndexes;
}


function freeFormLineBreaks(widths, lineBreakPoint) {
	var lineBreaks = [];
	var totals = [];
	var totalThisLine = 0;
	// run through each measure and see if the accumulation is less than the ideal.
	// if it passes the ideal, then see whether the last or this one is closer to the ideal.
	for (var i = 0; i < widths.length; i++) {
		var width = widths[i];
		var attemptedWidth = totalThisLine + width;
		if (attemptedWidth < lineBreakPoint)
			totalThisLine = attemptedWidth;
		else {
			// This just passed the ideal, so see whether the previous or the current number of measures is closer.
			var oldDistance = lineBreakPoint - totalThisLine;
			var newDistance = attemptedWidth - lineBreakPoint;
			if (oldDistance < newDistance && totalThisLine > 0) {
				lineBreaks.push(i - 1);
				totals.push(Math.round(totalThisLine - width));
				totalThisLine = width;
			} else {
				if (i < widths.length-1) {
					lineBreaks.push(i);
					totals.push(Math.round(totalThisLine));
					totalThisLine = 0;
				}
			}
		}
	}
	totals.push(Math.round(totalThisLine));
	return { lineBreaks: lineBreaks, totals: totals };
}

function clone(arr) {
	var newArr = [];
	for (var i = 0; i < arr.length; i++)
		newArr.push(arr[i]);
	return newArr;
}

function oneTry(measureWidths, idealWidths, accumulator, lineAccumulator, lineWidths, lastVariance, highestVariance, currLine, lineBreaks, startIndex, otherTries) {
	for (var i = startIndex; i < measureWidths.length; i++) {
		var measureWidth = measureWidths[i];
		accumulator += measureWidth;
		lineAccumulator += measureWidth;
		var thisVariance = Math.abs(accumulator - idealWidths[currLine]);
		var varianceIsClose = Math.abs(thisVariance - lastVariance) < idealWidths[0] / 10; // see if the difference is less than 10%, if so, run the test both ways.
		if (varianceIsClose) {
			if (thisVariance < lastVariance) {
				// Also attempt one less measure on the current line - sometimes that works out better.
				var newWidths = clone(lineWidths);
				var newBreaks = clone(lineBreaks);
				newBreaks.push(i-1);
				newWidths.push(lineAccumulator - measureWidth);
				otherTries.push({
					accumulator: accumulator,
					lineAccumulator: measureWidth,
					lineWidths: newWidths,
					lastVariance: Math.abs(accumulator - idealWidths[currLine+1]),
					highestVariance: Math.max(highestVariance, lastVariance),
					currLine: currLine+1,
					lineBreaks: newBreaks,
					startIndex: i+1});
			} else if (thisVariance > lastVariance && i < measureWidths.length-1) {
				// Also attempt one extra measure on this line.
				newWidths = clone(lineWidths);
				newBreaks = clone(lineBreaks);
				// newBreaks[newBreaks.length-1] = i;
				// newWidths[newWidths.length-1] = lineAccumulator;
				otherTries.push({
					accumulator: accumulator,
					lineAccumulator: lineAccumulator,
					lineWidths: newWidths,
					lastVariance: thisVariance,
					highestVariance: Math.max(highestVariance, thisVariance),
					currLine: currLine,
					lineBreaks: newBreaks,
					startIndex: i+1});
			}
		}
		if (thisVariance > lastVariance) {
			lineBreaks.push(i - 1);
			currLine++;
			highestVariance = Math.max(highestVariance, lastVariance);
			lastVariance = Math.abs(accumulator - idealWidths[currLine]);
			lineWidths.push(lineAccumulator - measureWidth);
			lineAccumulator = measureWidth;
		} else {
			lastVariance = thisVariance;
		}
	}
	lineWidths.push(lineAccumulator);
}

function optimizeLineWidths(widths, lineBreakPoint, lineBreaks, explanation) {
	//	figure out how many lines
	var numLines = Math.ceil(widths.total / lineBreakPoint); // + 1 TODO-PER: this used to be plus one - not sure why

	//	get the ideal width for a line (cumulative width / num lines) - approx the same as lineBreakPoint except for rounding
	var idealWidth = Math.floor(widths.total / numLines);

	//	get each ideal line width (1*ideal, 2*ideal, 3*ideal, etc)
	var idealWidths = [];
	for (var i = 0; i < numLines; i++)
		idealWidths.push(idealWidth*(i+1));

	//	from first measure, step through accum. Widths until the abs of the ideal is greater than the last one.
	// This can sometimes look funny in edge cases, so when the length is within 10%, try one more or one less to see which is better.
	// This is better than trying all the possibilities because that would get to be a huge number for even a medium size piece.
	// This method seems to never generate more than about 16 tries and it is usually 4 or less.
	var otherTries = [];
	otherTries.push({
		accumulator: 0,
		lineAccumulator: 0,
		lineWidths: [],
		lastVariance: 999999,
		highestVariance: 0,
		currLine: 0,
		lineBreaks: [], // These are the zero-based last measure on each line
		startIndex: 0});
	var index = 0;
	while (index < otherTries.length) {
		oneTry(widths.measureWidths,
			idealWidths,
			otherTries[index].accumulator,
			otherTries[index].lineAccumulator,
			otherTries[index].lineWidths,
			otherTries[index].lastVariance,
			otherTries[index].highestVariance,
			otherTries[index].currLine,
			otherTries[index].lineBreaks,
			otherTries[index].startIndex,
			otherTries);
		index++;
	}
	for (i = 0; i < otherTries.length; i++) {
		var otherTry = otherTries[i];
		otherTry.variances = [];
		otherTry.aveVariance = 0;
		for (var j = 0; j < otherTry.lineWidths.length; j++) {
			var lineWidth = otherTry.lineWidths[j];
			otherTry.variances.push(lineWidth - idealWidths[0]);
			otherTry.aveVariance += Math.abs(lineWidth - idealWidths[0]);
		}
		otherTry.aveVariance =  otherTry.aveVariance / otherTry.lineWidths.length;
		explanation.attempts.push({ type: "optimizeLineWidths", lineBreaks: otherTry.lineBreaks, variances: otherTry.variances, aveVariance: otherTry.aveVariance, widths: widths.measureWidths });
	}
	var smallest = 9999999;
	var smallestIndex = -1;
	for (i = 0; i < otherTries.length; i++) {
		otherTry = otherTries[i];
		if (otherTry.aveVariance < smallest) {
			smallest = otherTry.aveVariance;
			smallestIndex = i;
		}
	}
	return { failed: false, lineBreaks: otherTries[smallestIndex].lineBreaks, variance: otherTries[smallestIndex].highestVariance };
}

function fixedMeasureLineBreaks(widths, lineBreakPoint, preferredMeasuresPerLine) {
	var lineBreaks = [];
	var totals = [];
	var thisWidth = 0;
	var failed = false;
	for (var i = 0; i < widths.length; i++) {
		thisWidth += widths[i];
		if (thisWidth > lineBreakPoint) {
			failed = true;
		}
		if (i % preferredMeasuresPerLine === (preferredMeasuresPerLine-1)) {
			if (i !== widths.length-1) // Don't bother putting a line break for the last line - it's already a break.
				lineBreaks.push(i);
			totals.push(Math.round(thisWidth));
			thisWidth = 0;
		}
	}
	return { failed: failed, totals: totals, lineBreaks: lineBreaks };
}

function getRevisedTuneParams(lineBreaks, staffWidth, params) {

	var revisedParams = {
		lineBreaks: lineBreaks,
		staffwidth: staffWidth
	};
	for (var key in params) {
		if (params.hasOwnProperty(key) && key !== 'wrap' && key !== 'staffwidth') {
			revisedParams[key] = params[key];
		}
	}

	return { revisedParams: revisedParams };
}

function calcLineWraps(tune, widths, params) {
	// For calculating how much can go on the line, it depends on the width of the line. It is a convenience to just divide it here
	// by the minimum spacing instead of multiplying the min spacing later.
	// The scaling works differently: this is done by changing the scaling of the outer SVG, so the scaling needs to be compensated
	// for here, because the actual width will be different from the calculated numbers.

	// If the desired width is less than the margin, just punt and return the original tune
	//console.log(widths)
	if (widths.length === 0 || params.staffwidth < widths[0].left) {
		return {
			reParse: false,
			explanation: "Staff width is narrower than the margin",
			revisedParams: params
		};
	}
	var scale = params.scale ? Math.max(params.scale, 0.1) : 1;
	var minSpacing = params.wrap.minSpacing ? Math.max(parseFloat(params.wrap.minSpacing), 1) : 1;
	var minSpacingLimit = params.wrap.minSpacingLimit ? Math.max(parseFloat(params.wrap.minSpacingLimit), 1) : minSpacing - 0.1;
	var maxSpacing = params.wrap.maxSpacing ? Math.max(parseFloat(params.wrap.maxSpacing), 1) : undefined;
	if (params.wrap.lastLineLimit && !maxSpacing)
		maxSpacing = Math.max(parseFloat(params.wrap.lastLineLimit), 1);
	// var targetHeight = params.wrap.targetHeight ? Math.max(parseInt(params.wrap.targetHeight, 10), 100) : undefined;
	var preferredMeasuresPerLine = params.wrap.preferredMeasuresPerLine ? Math.max(parseInt(params.wrap.preferredMeasuresPerLine, 10), 0) : undefined;

	var accumulatedLineBreaks = [];
	var explanations = [];
	for (var s = 0; s < widths.length; s++) {
		var section = widths[s];
		var usableWidth = params.staffwidth - section.left;
		var lineBreakPoint = usableWidth / minSpacing / scale;
		var minLineSize = usableWidth / maxSpacing / scale;
		var allowableVariance = usableWidth / minSpacingLimit / scale;
		var explanation = {
			widths: section,
			lineBreakPoint: lineBreakPoint,
			minLineSize: minLineSize,
			attempts: [],
			staffWidth: params.staffwidth,
			minWidth: Math.round(allowableVariance)
		};

		// If there is a preferred number of measures per line, test that first. If none of the lines is too long, then we're finished.
		var lineBreaks = null;
		if (preferredMeasuresPerLine) {
			var f = fixedMeasureLineBreaks(section.measureWidths, lineBreakPoint, preferredMeasuresPerLine);
			explanation.attempts.push({
				type: "Fixed Measures Per Line",
				preferredMeasuresPerLine: preferredMeasuresPerLine,
				lineBreaks: f.lineBreaks,
				failed: f.failed,
				totals: f.totals
			});
			if (!f.failed)
				lineBreaks = f.lineBreaks;
		}

		// If we don't have lineBreaks yet, use the free form method of line breaks.
		// This will be called either if Preferred Measures is not used, or if the music is just weird - like a single measure is way too crowded.
		if (!lineBreaks) {
			var ff = freeFormLineBreaks(section.measureWidths, lineBreakPoint);
			explanation.attempts.push({type: "Free Form", lineBreaks: ff.lineBreaks, totals: ff.totals});
			lineBreaks = ff.lineBreaks;

			// We now have an acceptable number of lines, but the measures may not be optimally distributed. See if there is a better distribution.
			if (lineBreaks.length > 0 && section.measureWidths.length < 25) {
				// Only do this if everything doesn't fit on one line.
				// This is an intensive operation and it is optional so just do it for shorter music.
				ff = optimizeLineWidths(section, lineBreakPoint, lineBreaks, explanation);
				explanation.attempts.push({
					type: "Optimize",
					failed: ff.failed,
					reason: ff.reason,
					lineBreaks: ff.lineBreaks,
					totals: ff.totals
				});
				if (!ff.failed)
					lineBreaks = ff.lineBreaks;
			}
		}
		accumulatedLineBreaks.push(lineBreaks);
		explanations.push(explanation);
	}
	// If the vertical space exceeds targetHeight, remove a line and try again. If that is too crowded, then don't use it.
	var staffWidth = params.staffwidth;
	var ret = getRevisedTuneParams(accumulatedLineBreaks, staffWidth, params);
	ret.explanation = explanations;
	ret.reParse = true;
	return ret;
}

module.exports = { wrapLines: wrapLines, calcLineWraps: calcLineWraps };