abcjs 6.4.3 → 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,7 +26,7 @@ function CreateSynth() {
26
26
  self.audioBuffers = []; // cache of the buffers so starting play can be fast.
27
27
  self.duration = undefined; // the duration of the tune in seconds.
28
28
  self.isRunning = false; // whether there is currently a sound buffer running.
29
- self.options = undefined
29
+ self.options = {} // Thx tomohirohiratsuka
30
30
  self.pickupLength = 0
31
31
 
32
32
  // Load and cache all needed sounds
@@ -299,82 +299,91 @@ function CreateSynth() {
299
299
  return Promise.reject(new Error(notSupportedMessage));
300
300
  if (self.debugCallback)
301
301
  self.debugCallback("prime called");
302
- return new Promise(function(resolve) {
303
- var startTime = activeAudioContext().currentTime;
304
- var tempoMultiplier = self.millisecondsPerMeasure / 1000 / self.meterSize;
305
- self.duration = self.flattened.totalDuration * tempoMultiplier;
306
- if(self.duration <= 0) {
307
- self.audioBuffers = [];
308
- return resolve({ status: "empty", seconds: 0});
309
- }
310
- self.duration += fadeTimeSec;
311
- var totalSamples = Math.floor(activeAudioContext().sampleRate * self.duration);
312
302
 
313
- // There might be a previous run that needs to be turned off.
314
- self.stop();
303
+ return new Promise(function(resolve, reject) {
304
+ try {
305
+ var startTime = activeAudioContext().currentTime;
306
+ var tempoMultiplier = self.millisecondsPerMeasure / 1000 / self.meterSize;
307
+ self.duration = self.flattened.totalDuration * tempoMultiplier;
308
+ if (self.duration <= 0) {
309
+ self.audioBuffers = [];
310
+ return resolve({status: "empty", seconds: 0});
311
+ }
312
+ self.duration += fadeTimeSec;
313
+ var totalSamples = Math.floor(activeAudioContext().sampleRate * self.duration);
315
314
 
316
- var noteMapTracks = createNoteMap(self.flattened);
315
+ // There might be a previous run that needs to be turned off.
316
+ self.stop();
317
317
 
318
- if (self.options.swing)
319
- addSwing(noteMapTracks, self.options.swing, self.meterFraction, self.pickupLength)
318
+ var noteMapTracks = createNoteMap(self.flattened);
320
319
 
321
- if (self.sequenceCallback)
322
- self.sequenceCallback(noteMapTracks, self.callbackContext);
320
+ if (self.options.swing)
321
+ addSwing(noteMapTracks, self.options.swing, self.meterFraction, self.pickupLength)
323
322
 
324
- var panDistances = setPan(noteMapTracks.length, self.pan);
323
+ if (self.sequenceCallback)
324
+ self.sequenceCallback(noteMapTracks, self.callbackContext);
325
325
 
326
- // Create a simple list of all the unique sounds in this music and where they should be placed.
327
- // There appears to be a limit on how many audio buffers can be created at once so this technique limits the number needed.
328
- var uniqueSounds = {};
329
- noteMapTracks.forEach(function(noteMap, trackNumber) {
330
- var panDistance = panDistances && panDistances.length > trackNumber ? panDistances[trackNumber] : 0;
331
- noteMap.forEach(function(note) {
332
- var key = note.instrument + ':' + note.pitch + ':' +note.volume + ':' + Math.round((note.end-note.start)*1000)/1000 + ':' + panDistance + ':' + tempoMultiplier + ':' + (note.cents ? note.cents : 0);
333
- if (self.debugCallback)
334
- self.debugCallback("noteMapTrack "+key)
335
- if (!uniqueSounds[key])
336
- uniqueSounds[key] = [];
337
- uniqueSounds[key].push(note.start);
326
+ var panDistances = setPan(noteMapTracks.length, self.pan);
327
+
328
+ // Create a simple list of all the unique sounds in this music and where they should be placed.
329
+ // There appears to be a limit on how many audio buffers can be created at once so this technique limits the number needed.
330
+ var uniqueSounds = {};
331
+ noteMapTracks.forEach(function (noteMap, trackNumber) {
332
+ var panDistance = panDistances && panDistances.length > trackNumber ? panDistances[trackNumber] : 0;
333
+ noteMap.forEach(function (note) {
334
+ var key = note.instrument + ':' + note.pitch + ':' + note.volume + ':' + Math.round((note.end - note.start) * 1000) / 1000 + ':' + panDistance + ':' + tempoMultiplier + ':' + (note.cents ? note.cents : 0);
335
+ if (self.debugCallback)
336
+ self.debugCallback("noteMapTrack " + key)
337
+ if (!uniqueSounds[key])
338
+ uniqueSounds[key] = [];
339
+ uniqueSounds[key].push(note.start);
340
+ });
338
341
  });
339
- });
340
342
 
341
- // Now that we know what we are trying to create, construct the audio buffer by creating each sound and placing it.
342
- var allPromises = [];
343
- var audioBuffer = activeAudioContext().createBuffer(2, totalSamples, activeAudioContext().sampleRate);
344
- for (var key2 = 0; key2 < Object.keys(uniqueSounds).length; key2++) {
345
- var k = Object.keys(uniqueSounds)[key2];
346
- var parts = k.split(":");
347
- var cents = parts[6] !== undefined ? parseFloat(parts[6]) : 0;
348
- parts = {instrument: parts[0], pitch: parseInt(parts[1], 10), volume: parseInt(parts[2], 10), len: parseFloat(parts[3]), pan: parseFloat(parts[4]), tempoMultiplier: parseFloat(parts[5]), cents: cents};
349
- allPromises.push(placeNote(audioBuffer, activeAudioContext().sampleRate, parts, uniqueSounds[k], self.soundFontVolumeMultiplier, self.programOffsets[parts.instrument], fadeTimeSec, self.noteEnd/1000, self.debugCallback));
350
- }
351
- self.audioBuffers = [audioBuffer];
343
+ // Now that we know what we are trying to create, construct the audio buffer by creating each sound and placing it.
344
+ var allPromises = [];
345
+ var audioBuffer = activeAudioContext().createBuffer(2, totalSamples, activeAudioContext().sampleRate);
346
+ for (var key2 = 0; key2 < Object.keys(uniqueSounds).length; key2++) {
347
+ var k = Object.keys(uniqueSounds)[key2];
348
+ var parts = k.split(":");
349
+ var cents = parts[6] !== undefined ? parseFloat(parts[6]) : 0;
350
+ parts = {instrument: parts[0], pitch: parseInt(parts[1], 10), volume: parseInt(parts[2], 10), len: parseFloat(parts[3]), pan: parseFloat(parts[4]), tempoMultiplier: parseFloat(parts[5]), cents: cents};
351
+ allPromises.push(placeNote(audioBuffer, activeAudioContext().sampleRate, parts, uniqueSounds[k], self.soundFontVolumeMultiplier, self.programOffsets[parts.instrument], fadeTimeSec, self.noteEnd / 1000, self.debugCallback));
352
+ }
353
+ self.audioBuffers = [audioBuffer];
352
354
 
353
- if (self.debugCallback) {
354
- self.debugCallback("sampleRate = " + activeAudioContext().sampleRate);
355
- self.debugCallback("totalSamples = " + totalSamples);
356
- self.debugCallback("creationTime = " + Math.floor((activeAudioContext().currentTime - startTime)*1000) + "ms");
357
- }
358
- function resolveData(me) {
359
- var duration = me && me.audioBuffers && me.audioBuffers.length > 0 ? me.audioBuffers[0].duration : 0;
360
- return { status: activeAudioContext().state, duration: duration}
361
- }
362
- Promise.all(allPromises).then(function() {
363
- // Safari iOS can mess with the audioContext state, so resume if needed.
364
- if (activeAudioContext().state === "suspended") {
365
- activeAudioContext().resume().then(function () {
366
- resolve(resolveData(self));
367
- })
368
- } else if (activeAudioContext().state === "interrupted") {
369
- activeAudioContext().suspend().then(function () {
355
+ if (self.debugCallback) {
356
+ self.debugCallback("sampleRate = " + activeAudioContext().sampleRate);
357
+ self.debugCallback("totalSamples = " + totalSamples);
358
+ self.debugCallback("creationTime = " + Math.floor((activeAudioContext().currentTime - startTime) * 1000) + "ms");
359
+ }
360
+
361
+ function resolveData(me) {
362
+ var duration = me && me.audioBuffers && me.audioBuffers.length > 0 ? me.audioBuffers[0].duration : 0;
363
+ return {status: activeAudioContext().state, duration: duration}
364
+ }
365
+
366
+ Promise.all(allPromises).then(function () {
367
+ // Safari iOS can mess with the audioContext state, so resume if needed.
368
+ if (activeAudioContext().state === "suspended") {
370
369
  activeAudioContext().resume().then(function () {
371
370
  resolve(resolveData(self));
372
371
  })
373
- })
374
- } else {
375
- resolve(resolveData(self));
376
- }
377
- });
372
+ } else if (activeAudioContext().state === "interrupted") {
373
+ activeAudioContext().suspend().then(function () {
374
+ activeAudioContext().resume().then(function () {
375
+ resolve(resolveData(self));
376
+ })
377
+ })
378
+ } else {
379
+ resolve(resolveData(self));
380
+ }
381
+ }).catch(function (error) {
382
+ reject(error)
383
+ });
384
+ } catch (error) {
385
+ reject(error)
386
+ }
378
387
  });
379
388
  };
380
389
 
@@ -101,7 +101,7 @@ function placeNote(outputAudioBuffer, sampleRate, sound, startArray, volumeMulti
101
101
  .catch(function (error) {
102
102
  if (debugCallback)
103
103
  debugCallback('placeNote catch: '+error.message)
104
- return Promise.resolve()
104
+ return Promise.reject(error)
105
105
  });
106
106
  }
107
107
 
@@ -104,7 +104,7 @@ var midiSequencerLint = function(tune) {
104
104
  ret += element.channel;
105
105
  ret += '\n';
106
106
  break;
107
- case "gchord":
107
+ case "gchordOn":
108
108
  ret += "\t\t";
109
109
  ret += element.tacet ? 'tacet' : 'on';
110
110
  ret += '\n';
@@ -53,7 +53,7 @@ var JSONSchema = require('./jsonschema-b4');
53
53
  var ParserLint = function() {
54
54
  "use strict";
55
55
  var decorationList = { type: 'array', optional: true, items: { type: 'string', Enum: [
56
- "trill", "lowermordent", "uppermordent", "mordent", "pralltriller", "accent",
56
+ "trill", "trillh", "lowermordent", "uppermordent", "mordent", "pralltriller", "accent",
57
57
  "fermata", "invertedfermata", "tenuto", "0", "1", "2", "3", "4", "5", "+", "wedge",
58
58
  "open", "thumb", "snap", "turn", "roll", "irishroll", "breath", "shortphrase", "mediumphrase", "longphrase",
59
59
  "segno", "coda", "D.S.", "D.C.", "fine", "crescendo(", "crescendo)", "diminuendo(", "diminuendo)", "glissando(", "glissando)",
@@ -943,7 +943,8 @@ AbstractEngraver.prototype.addMeasureNumber = function (number, abselem) {
943
943
  var dx = 0;
944
944
  if (abselem.isClef) // If this is a clef rather than bar line, then the number shouldn't be centered because it could overlap the left side. This is an easy way to let it be centered but move it over, too.
945
945
  dx += measureNumDim.width / 2
946
- var vert = measureNumDim.width > 10 && abselem.abcelem.type === "treble" ? 13 : 11
946
+ // MAE 1 Oct 2024 - Change 13 to 13.5 since previously bar numbers were very slightly overlapping the top of the clef
947
+ var vert = measureNumDim.width > 10 && abselem.abcelem.type === "treble" ? 13.5 : 11
947
948
  abselem.addFixed(new RelativeElement(number, dx, measureNumDim.width, vert + measureNumDim.height / spacing.STEP, { type: "barNumber", dim: this.getTextSize.attr("measurefont", 'bar-number') }));
948
949
  };
949
950
 
@@ -1025,13 +1026,16 @@ AbstractEngraver.prototype.createBarLine = function (voice, elem, isFirstStaff)
1025
1026
  abselem.addRight(new RelativeElement("dots.dot", dx, 1, 5));
1026
1027
  } // 2 is hardcoded
1027
1028
 
1028
- if (elem.startEnding && isFirstStaff) { // only put the first & second ending marks on the first staff
1029
- var textWidth = this.getTextSize.calc(elem.startEnding, "repeatfont", '').width;
1030
- abselem.minspacing += textWidth + 10; // Give plenty of room for the ending number.
1031
- this.partstartelem = new EndingElem(elem.startEnding, anchor, null);
1032
- voice.addOther(this.partstartelem);
1029
+ if (elem.startEnding && isFirstStaff) {
1030
+ // MAE 17 May 2025 - Fixes drawing issue
1031
+ if (voice.voicenumber === 0){
1032
+ // only put the first & second ending marks on the first staff
1033
+ var textWidth = this.getTextSize.calc(elem.startEnding, "repeatfont", '').width;
1034
+ abselem.minspacing += textWidth + 10; // Give plenty of room for the ending number.
1035
+ this.partstartelem = new EndingElem(elem.startEnding, anchor, null);
1036
+ voice.addOther(this.partstartelem);
1037
+ }
1033
1038
  }
1034
-
1035
1039
  // Add a little space to the left of the bar line so that nothing can crowd it.
1036
1040
  abselem.extraw -= 5;
1037
1041
 
@@ -175,6 +175,7 @@ var stackedDecoration = function (decoration, width, abselem, yPos, positioning,
175
175
  "mediumphrase": "scripts.mediumphrase",
176
176
  "longphrase": "scripts.longphrase",
177
177
  "trill": "scripts.trill",
178
+ "trillh": "scripts.trill",
178
179
  "roll": "scripts.roll",
179
180
  "irishroll": "scripts.roll",
180
181
  "marcato": "scripts.umarcato",
@@ -237,6 +238,7 @@ var stackedDecoration = function (decoration, width, abselem, yPos, positioning,
237
238
  case "mediumphrase":
238
239
  case "longphrase":
239
240
  case "trill":
241
+ case "trillh":
240
242
  case "roll":
241
243
  case "irishroll":
242
244
  case "marcato":
@@ -177,22 +177,33 @@ AbsoluteElement.prototype.setLimit = function (member, child) {
177
177
  };
178
178
 
179
179
  AbsoluteElement.prototype._addChild = function (child) {
180
- // console.log("Relative:",child);
181
- child.parent = this;
182
- this.children[this.children.length] = child;
183
- this.pushTop(child.top);
184
- this.pushBottom(child.bottom);
185
- this.setLimit('tempoHeightAbove', child);
186
- this.setLimit('partHeightAbove', child);
187
- this.setLimit('volumeHeightAbove', child);
188
- this.setLimit('dynamicHeightAbove', child);
189
- this.setLimit('endingHeightAbove', child);
190
- this.setLimit('chordHeightAbove', child);
191
- this.setLimit('lyricHeightAbove', child);
192
- this.setLimit('lyricHeightBelow', child);
193
- this.setLimit('chordHeightBelow', child);
194
- this.setLimit('volumeHeightBelow', child);
195
- this.setLimit('dynamicHeightBelow', child);
180
+ // console.log("Relative:",child);
181
+
182
+ // MAE 30 Sep 2024 - To avoid extra space for chords if there is only a bar number on the clef
183
+ var okToPushTop = true;
184
+ if ((this.abcelem.el_type == "clef") && (child.type == "barNumber")){
185
+ okToPushTop = false;
186
+ }
187
+
188
+ child.parent = this;
189
+ this.children[this.children.length] = child;
190
+
191
+ if (okToPushTop){
192
+ this.pushTop(child.top);
193
+ }
194
+
195
+ this.pushBottom(child.bottom);
196
+ this.setLimit('tempoHeightAbove', child);
197
+ this.setLimit('partHeightAbove', child);
198
+ this.setLimit('volumeHeightAbove', child);
199
+ this.setLimit('dynamicHeightAbove', child);
200
+ this.setLimit('endingHeightAbove', child);
201
+ this.setLimit('chordHeightAbove', child);
202
+ this.setLimit('lyricHeightAbove', child);
203
+ this.setLimit('lyricHeightBelow', child);
204
+ this.setLimit('chordHeightBelow', child);
205
+ this.setLimit('volumeHeightBelow', child);
206
+ this.setLimit('dynamicHeightBelow', child);
196
207
  };
197
208
 
198
209
  AbsoluteElement.prototype.pushTop = function (top) {
@@ -10,7 +10,12 @@ function FreeText(info, vskip, getFontAndAttr, paddingLeft, width, getTextSize)
10
10
  } else if (typeof text === 'string') {
11
11
  this.rows.push({ move: hash.attr['font-size'] / 2 }); // TODO-PER: move down some - the y location should be the top of the text, but we output text specifying the center line.
12
12
  this.rows.push({ left: paddingLeft, text: text, font: 'textfont', klass: 'defined-text', anchor: "start", startChar: info.startChar, endChar: info.endChar, absElemType: "freeText", name: "free-text" });
13
- size = getTextSize.calc(text, 'textfont', 'defined-text');
13
+ // MAE 9 May 2025 - Force blank text lines in a text block to have height
14
+ function replaceStandaloneNewlinesForTextBlocks(input) {
15
+ return input.replace(/^[ \t]*\n/gm, 'X\n');;
16
+ }
17
+ var textForSize = replaceStandaloneNewlinesForTextBlocks(text);
18
+ size = getTextSize.calc(textForSize, 'textfont', 'defined-text'); // was text
14
19
  this.rows.push({ move: size.height });
15
20
  } else if (text) {
16
21
  var maxHeight = 0;
@@ -15,10 +15,18 @@ function draw(renderer, classes, abcTune, width, maxWidth, responsive, scale, se
15
15
  renderer.paper.closeGroup()
16
16
  renderer.moveY(renderer.spacing.music);
17
17
  var staffgroups = [];
18
+ var nStaves = 0;
18
19
  for (var line = 0; line < abcTune.lines.length; line++) {
19
20
  classes.incrLine();
20
21
  var abcLine = abcTune.lines[line];
21
22
  if (abcLine.staff) {
23
+ // MAE 26 May 2025 - for incipits staff count limiting
24
+ nStaves++;
25
+ if (abcTune.formatting.maxStaves){
26
+ if (nStaves > abcTune.formatting.maxStaves){
27
+ break;
28
+ }
29
+ }
22
30
  if (classes.shouldAddClasses)
23
31
  groupClasses.klass = "abcjs-staff-wrapper abcjs-l" + classes.lineNumber
24
32
  renderer.paper.openGroup(groupClasses)
@@ -24,9 +24,18 @@ function drawEnding(renderer, params, linestartx, lineendx, selectables) {
24
24
 
25
25
  pathString += sprintf("M %f %f L %f %f ",
26
26
  linestartx, y, lineendx, y);
27
-
28
- renderer.paper.openGroup({ klass: renderer.controller.classes.generate("ending"), "data-name": "ending" });
29
- printPath(renderer, { path: pathString, stroke: renderer.foregroundColor, fill: renderer.foregroundColor, "data-name": "line" });
27
+ renderer.paper.openGroup({
28
+ klass: renderer.controller.classes.generate("ending"),
29
+ // MAE 17 May 2025 - Ending numbers not being drawn in correct color
30
+ fill: renderer.foregroundColor,
31
+ "data-name": "ending"
32
+ });
33
+ printPath(renderer, {
34
+ path: pathString,
35
+ stroke: renderer.foregroundColor,
36
+ fill: renderer.foregroundColor,
37
+ "data-name": "line"
38
+ });
30
39
  if (params.anchor1)
31
40
  renderText(renderer, {
32
41
  x: roundNumber(linestartx + 5),
@@ -37,7 +37,14 @@ function renderText(renderer, params, alreadyInGroup) {
37
37
  hash.attr.cursor = params.cursor;
38
38
  }
39
39
 
40
- var text = params.text.replace(/\n\n/g, "\n \n");
40
+ // MAE 9 May 2025 for free text blocks
41
+ var text;
42
+ if (params.name == "free-text"){
43
+ text = params.text.replace(/^[ \t]*\n/gm, ' \n');
44
+ }
45
+ else{
46
+ text = params.text.replace(/\n\n/g, "\n \n");
47
+ }
41
48
  text = text.replace(/^\n/, "\xA0\n");
42
49
 
43
50
  if (hash.font.box) {
@@ -130,8 +130,14 @@ function keyboardSelection(ev) {
130
130
  function findElementInHistory(selectables, el) {
131
131
  if (!el)
132
132
  return -1;
133
+ // This should always exist, but it occasionally causes an exception, so check first.
134
+ var dataset = el.dataset
135
+ if (!dataset)
136
+ return -1
137
+ var index = dataset.index
133
138
  for (var i = 0; i < selectables.length; i++) {
134
- if (el.dataset.index === selectables[i].svgEl.dataset.index)
139
+ var svgDataset = selectables[i].svgEl.dataset
140
+ if (svgDataset && index === svgDataset.index)
135
141
  return i;
136
142
  }
137
143
  return -1;
package/src/write/svg.js CHANGED
@@ -176,8 +176,14 @@ Svg.prototype.text = function (text, attr, target) {
176
176
  el.setAttribute(key, attr[key]);
177
177
  }
178
178
  }
179
+ var isFreeText = (attr["data-name"] == "free-text");
179
180
  var lines = ("" + text).split("\n");
180
181
  for (var i = 0; i < lines.length; i++) {
182
+ if (isFreeText && (lines[i] == "")){
183
+ // Don't draw empty lines
184
+ continue;
185
+ }
186
+
181
187
  var line = document.createElementNS(svgNS, 'tspan');
182
188
  line.setAttribute("x", attr.x ? attr.x : 0);
183
189
  if (i !== 0)
@@ -200,8 +206,23 @@ Svg.prototype.text = function (text, attr, target) {
200
206
  ts3.textContent = parts[2];
201
207
  line.appendChild(ts3);
202
208
  }
203
- } else
204
- line.textContent = lines[i];
209
+ }
210
+ else
211
+ {
212
+ // MAE 9 May 2025 - For improved block text
213
+ if (isFreeText){
214
+ // Fixes issue where blank lines in text blocks didn't take up any vertical
215
+ if (lines[i].trim() == ""){
216
+ line.innerHTML = "&nbsp;";
217
+ }
218
+ else{
219
+ line.textContent = lines[i];
220
+ }
221
+ }
222
+ else{
223
+ line.textContent = lines[i];
224
+ }
225
+ }
205
226
  el.appendChild(line);
206
227
  }
207
228
  if (target)
package/types/index.d.ts CHANGED
@@ -61,7 +61,7 @@ declare module 'abcjs' {
61
61
 
62
62
  export type NoteHeadType = 'normal' | 'harmonic' | 'rhythm' | 'x' | 'triangle';
63
63
 
64
- export type Decorations = "trill" | "lowermordent" | "uppermordent" | "mordent" | "pralltriller" | "accent" |
64
+ export type Decorations = "trill" | "trillh" | "lowermordent" | "uppermordent" | "mordent" | "pralltriller" | "accent" |
65
65
  "fermata" | "invertedfermata" | "tenuto" | "0" | "1" | "2" | "3" | "4" | "5" | "+" | "wedge" |
66
66
  "open" | "thumb" | "snap" | "turn" | "roll" | "irishroll" | "breath" | "shortphrase" | "mediumphrase" | "longphrase" |
67
67
  "segno" | "coda" | "D.S." | "D.C." | "fine" | "crescendo(" | "crescendo)" | "diminuendo(" | "diminuendo)" |"glissando(" | "glissando)" |
package/version.js CHANGED
@@ -1,3 +1,3 @@
1
- var version = '6.4.3';
1
+ var version = '6.5.0';
2
2
 
3
3
  module.exports = version;