Skip to content

feat: add support for multiple tempo changes in bar #1600

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions playground-template/alphatex-editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,13 @@
req.onload = data => {
document.getElementById('placeholder').outerHTML = req.responseText;

const element = document.getElementById('alphaTab');
const element = document.getElementById('alphaTabControl');
delete element.dataset.file;
delete element.dataset.tracks;
element.dataset.tex = true;
element.innerHTML = document.getElementById('editor').innerHTML;

window.at = setupControl('#alphaTab', { file: undefined, tex: ''});
window.at = setupControl('#alphaTabControl', { file: undefined, tex: ''});
setupEditor(window.at, '#editor');
};
req.open('GET', 'control-template.html');
Expand Down
13 changes: 13 additions & 0 deletions src.csharp/AlphaTab.Test/Test/Globals.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace AlphaTab.Test
Expand Down Expand Up @@ -66,6 +67,7 @@ public Expector(T actual)
public Expector<T> To => this;
public NotExpector<T> Not => new(_actual);
public Expector<T> Be => this;
public Expector<T> Have => this;

public void Equal(object? expected, string? message = null)
{
Expand Down Expand Up @@ -108,6 +110,17 @@ public void Ok()
{
Assert.AreNotEqual(default!, _actual);
}
public void Length(int length)
{
if(_actual is ICollection collection)
{
Assert.AreEqual(length, collection.Count);
}
else
{
Assert.Fail("Length can only be used with collection operands");
}
}

public void True()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ class Expector<T>(private val actual: T) {
val be
get() = this

val have
get() = this

fun equal(expected: Any?, message: String? = null) {
var expectedTyped: Any? = expected

Expand All @@ -66,6 +69,18 @@ class Expector<T>(private val actual: T) {
}
}

fun length(expected:Double) {
if(actual is alphaTab.collections.List<*>) {
kotlin.test.assertEquals(expected.toInt(), actual.length.toInt())
} else if(actual is alphaTab.collections.DoubleList) {
kotlin.test.assertEquals(expected.toInt(), actual.length.toInt())
} else if(actual is alphaTab.collections.BooleanList) {
kotlin.test.assertEquals(expected.toInt(), actual.length.toInt())
} else {
kotlin.test.fail("length can only be used with collection operands");
}
}

// fun toBe(expected: Any) {
// var expectedTyped = expected
// if (expected is Int && actual is Double) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ public class List<T> : Iterable<T> {
return List(_data.subList(start.toInt(), _data.size))
}

public fun shift(): T {
return _data.removeAt(0)
}

public fun splice(start: Double, deleteCount: Double, vararg newElements: T) {
var actualStart = start.toInt()
if (actualStart < 0)
Expand Down
52 changes: 26 additions & 26 deletions src/exporter/GpifWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -944,28 +944,30 @@ export class GpifWriter {
masterTrackNode.addElement('Anacrusis');
}

const initialTempoAutomation = automations.addElement('Automation');
initialTempoAutomation.addElement('Type').innerText = 'Tempo';
initialTempoAutomation.addElement('Linear').innerText = 'false';
initialTempoAutomation.addElement('Bar').innerText = '0';
initialTempoAutomation.addElement('Position').innerText = '0';
initialTempoAutomation.addElement('Visible').innerText = 'true';
initialTempoAutomation.addElement('Value').innerText = `${score.tempo} 2`;
if (score.tempoLabel) {
initialTempoAutomation.addElement('Text').innerText = score.tempoLabel;
if(score.masterBars[0].tempoAutomations.length === 0){
const initialTempoAutomation = automations.addElement('Automation');
initialTempoAutomation.addElement('Type').innerText = 'Tempo';
initialTempoAutomation.addElement('Linear').innerText = 'false';
initialTempoAutomation.addElement('Bar').innerText = '0';
initialTempoAutomation.addElement('Position').innerText = '0';
initialTempoAutomation.addElement('Visible').innerText = 'true';
initialTempoAutomation.addElement('Value').innerText = `${score.tempo} 2`;
if (score.tempoLabel) {
initialTempoAutomation.addElement('Text').innerText = score.tempoLabel;
}
}

for (const mb of score.masterBars) {
if (mb.index > 0 && mb.tempoAutomation) {
for (const automation of mb.tempoAutomations) {
const tempoAutomation = automations.addElement('Automation');
tempoAutomation.addElement('Type').innerText = 'Tempo';
tempoAutomation.addElement('Linear').innerText = mb.tempoAutomation.isLinear ? 'true' : 'false';
tempoAutomation.addElement('Linear').innerText = automation.isLinear ? 'true' : 'false';
tempoAutomation.addElement('Bar').innerText = mb.index.toString();
tempoAutomation.addElement('Position').innerText = mb.tempoAutomation.ratioPosition.toString();
tempoAutomation.addElement('Position').innerText = automation.ratioPosition.toString();
tempoAutomation.addElement('Visible').innerText = 'true';
tempoAutomation.addElement('Value').innerText = `${mb.tempoAutomation.value} 2`;
if (mb.tempoAutomation.text) {
tempoAutomation.addElement('Text').innerText = mb.tempoAutomation.text;
tempoAutomation.addElement('Value').innerText = `${automation.value} 2`;
if (automation.text) {
tempoAutomation.addElement('Text').innerText = automation.text;
}
}
}
Expand Down Expand Up @@ -1255,7 +1257,7 @@ export class GpifWriter {
let chordFret = chord.strings[i];
if (chordFret !== -1) {
const fretNode = diagram.addElement('Fret');
const chordString = (chord.strings.length - 1 - i);
const chordString = chord.strings.length - 1 - i;
fretNode.attributes.set('string', chordString.toString());
fretNode.attributes.set('fret', (chordFret - chord.firstFret + 1).toString());
if (!fretToStrings.has(chordFret)) {
Expand All @@ -1275,7 +1277,7 @@ export class GpifWriter {
Fingers.LittleFinger,
Fingers.AnnularFinger,
Fingers.MiddleFinger,
Fingers.IndexFinger,
Fingers.IndexFinger
];

for (const fret of frets) {
Expand Down Expand Up @@ -1305,22 +1307,20 @@ export class GpifWriter {
}
}


const showName = diagram.addElement('Property');
showName.attributes.set('name', 'ShowName');
showName.attributes.set('type', 'bool');
showName.attributes.set('value', chord.showName ? "true" : "false");
showName.attributes.set('value', chord.showName ? 'true' : 'false');

const showDiagram = diagram.addElement('Property');
showDiagram.attributes.set('name', 'ShowDiagram');
showDiagram.attributes.set('type', 'bool');
showDiagram.attributes.set('value', chord.showDiagram ? "true" : "false");
showDiagram.attributes.set('value', chord.showDiagram ? 'true' : 'false');

const showFingering = diagram.addElement('Property');
showFingering.attributes.set('name', 'ShowFingering');
showFingering.attributes.set('type', 'bool');
showFingering.attributes.set('value', chord.showFingering ? "true" : "false");

showFingering.attributes.set('value', chord.showFingering ? 'true' : 'false');

// TODO Chord details
const chordNode = diagram.addElement('Chord');
Expand Down Expand Up @@ -1439,8 +1439,8 @@ export class GpifWriter {

instrumentSet.addElement('Name').innerText = GpifWriter.DrumKitProgramInfo.instrumentSetName;
instrumentSet.addElement('Type').innerText = GpifWriter.DrumKitProgramInfo.instrumentSetType;
let currentElementType: string = "";
let currentElementName: string = "";
let currentElementType: string = '';
let currentElementName: string = '';
let currentArticulations: XmlNode = new XmlNode();
let counterPerType = new Map<string, number>();
const elements = instrumentSet.addElement('Elements');
Expand Down Expand Up @@ -1600,7 +1600,7 @@ export class GpifWriter {
}

private writeFermatas(parent: XmlNode, masterBar: MasterBar) {
const fermataCount = (masterBar.fermata?.size ?? 0);
const fermataCount = masterBar.fermata?.size ?? 0;
if (fermataCount === 0) {
return;
}
Expand Down
20 changes: 9 additions & 11 deletions src/generated/model/MasterBarSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class MasterBarSerializer {
o.set("timesignaturecommon", obj.timeSignatureCommon);
o.set("tripletfeel", obj.tripletFeel as number);
o.set("section", SectionSerializer.toJson(obj.section));
o.set("tempoautomation", AutomationSerializer.toJson(obj.tempoAutomation));
o.set("tempoautomations", obj.tempoAutomations.map(i => AutomationSerializer.toJson(i)));
if (obj.fermata !== null) {
const m = new Map<string, unknown>();
o.set("fermata", m);
Expand Down Expand Up @@ -83,6 +83,14 @@ export class MasterBarSerializer {
case "tripletfeel":
obj.tripletFeel = JsonHelper.parseEnum<TripletFeel>(v, TripletFeel)!;
return true;
case "tempoautomations":
obj.tempoAutomations = [];
for (const o of (v as (Map<string, unknown> | null)[])) {
const i = new Automation();
AutomationSerializer.fromJson(i, o);
obj.tempoAutomations.push(i);
}
return true;
case "fermata":
obj.fermata = new Map<number, Fermata>();
JsonHelper.forEach(v, (v, k) => {
Expand Down Expand Up @@ -114,16 +122,6 @@ export class MasterBarSerializer {
}
return true;
}
if (["tempoautomation"].indexOf(property) >= 0) {
if (v) {
obj.tempoAutomation = new Automation();
AutomationSerializer.fromJson(obj.tempoAutomation, v as Map<string, unknown>);
}
else {
obj.tempoAutomation = null;
}
return true;
}
return false;
}
}
39 changes: 25 additions & 14 deletions src/importer/AlphaTexImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1500,6 +1500,12 @@ export class AlphaTexImporter extends ScoreImporter {
beat.crescendo = CrescendoType.Crescendo;
} else if (syData === 'dec') {
beat.crescendo = CrescendoType.Decrescendo;
} else if(syData === 'tempo') {
// NOTE: playbackRatio is calculated on score finish when playback positions are known
const tempoAutomation = this.readTempoAutomation();
beat.automations.push(tempoAutomation);
beat.voice.bar.masterBar.tempoAutomations.push(tempoAutomation);
return true;
} else if (syData === 'tp') {
this._sy = this.newSy();
beat.tremoloSpeed = Duration.Eighth;
Expand Down Expand Up @@ -1977,18 +1983,8 @@ export class AlphaTexImporter extends ScoreImporter {
}
this._sy = this.newSy();
} else if (syData === 'tempo') {
this._allowFloat = true;
this._sy = this.newSy();
this._allowFloat = false;
if (this._sy !== AlphaTexSymbols.Number) {
this.error('tempo', AlphaTexSymbols.Number, true);
}
let tempoAutomation: Automation = new Automation();
tempoAutomation.isLinear = false;
tempoAutomation.type = AutomationType.Tempo;
tempoAutomation.value = this._syData as number;
master.tempoAutomation = tempoAutomation;
this._sy = this.newSy();
const tempoAutomation = this.readTempoAutomation();
master.tempoAutomations.push(tempoAutomation);
} else if (syData === 'section') {
this._sy = this.newSy();
if (this._sy !== AlphaTexSymbols.String) {
Expand Down Expand Up @@ -2036,16 +2032,31 @@ export class AlphaTexImporter extends ScoreImporter {
}
}

if (master.index === 0 && !master.tempoAutomation) {
if (master.index === 0 && master.tempoAutomations.length === 0) {
let tempoAutomation: Automation = new Automation();
tempoAutomation.isLinear = false;
tempoAutomation.type = AutomationType.Tempo;
tempoAutomation.value = this._score.tempo;
master.tempoAutomation = tempoAutomation;
master.tempoAutomations.push(tempoAutomation);
}
return anyMeta;
}

private readTempoAutomation() {
this._allowFloat = true;
this._sy = this.newSy();
this._allowFloat = false;
if (this._sy !== AlphaTexSymbols.Number) {
this.error('tempo', AlphaTexSymbols.Number, true);
}
const tempoAutomation: Automation = new Automation();
tempoAutomation.isLinear = false;
tempoAutomation.type = AutomationType.Tempo;
tempoAutomation.value = this._syData as number;
this._sy = this.newSy();
return tempoAutomation;
}

private applyAlternateEnding(master: MasterBar): void {
let num = this._syData as number;
if (num < 1) {
Expand Down
20 changes: 6 additions & 14 deletions src/importer/CapellaParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ export class CapellaParser {
// voice counts and contents might be inconsistent
// we need to ensure we have an equal amount of voices across all bars
// and voices must contain an empty beat at minimum
let tempo = this.score.tempo;
for (const track of this.score.tracks) {
const trackVoiceCount = this._voiceCounts.get(track.index)!;
for (const staff of track.staves) {
Expand All @@ -160,15 +159,6 @@ export class CapellaParser {
voice.addBeat(emptyBeat);
}
}

const mb = bar.masterBar;
if (mb.tempoAutomation) {
if (mb.tempoAutomation.value !== tempo) {
tempo = mb.tempoAutomation.value;
} else {
mb.tempoAutomation = null;
}
}
}
}
}
Expand Down Expand Up @@ -619,10 +609,12 @@ export class CapellaParser {
const noteObjects = element.findChildElement('noteObjects');

if (systemElement.attributes.has('tempo')) {
this._currentBar.masterBar.tempoAutomation = new Automation();
this._currentBar.masterBar.tempoAutomation.isLinear = true;
this._currentBar.masterBar.tempoAutomation.type = AutomationType.Tempo;
this._currentBar.masterBar.tempoAutomation.value = parseInt(systemElement.attributes.get('tempo')!);
const automation = new Automation()
automation.isLinear = true;
automation.type = AutomationType.Tempo;
automation.value = parseInt(systemElement.attributes.get('tempo')!);
automation.ratioPosition = this._currentVoiceState.currentPosition / this._currentVoiceState.currentBarDuration;
this._currentBar.masterBar.tempoAutomations.push(automation);
}

if (noteObjects) {
Expand Down
7 changes: 4 additions & 3 deletions src/importer/Gp3To5Importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,9 @@ export class Gp3To5Importer extends ScoreImporter {
// To be more in line with the GP7 structure we create an
// initial tempo automation on the first masterbar
if (this._score.masterBars.length > 0) {
this._score.masterBars[0].tempoAutomation = Automation.buildTempoAutomation(false, 0, this._score.tempo, 2);
this._score.masterBars[0].tempoAutomation.text = this._score.tempoLabel;
const automation = Automation.buildTempoAutomation(false, 0, this._score.tempo, 2);;
automation.text = this._score.tempoLabel;
this._score.masterBars[0].tempoAutomations.push(automation);
}

this._score.finish(this.settings);
Expand Down Expand Up @@ -880,7 +881,7 @@ export class Gp3To5Importer extends ScoreImporter {
tempoAutomation.type = AutomationType.Tempo;
tempoAutomation.value = tableChange.tempo;
beat.automations.push(tempoAutomation);
beat.voice.bar.masterBar.tempoAutomation = tempoAutomation;
beat.voice.bar.masterBar.tempoAutomations.push(tempoAutomation);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/importer/GpifParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2332,7 +2332,7 @@ export class GpifParser {
this.score.tempoLabel = automation.text;
}
}
masterBar.tempoAutomation = automation;
masterBar.tempoAutomations.push(automation);
}
}
}
Expand Down
Loading