Skip to content
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

Fix MusicXML Slurs and Reworked Repeats #898

Merged
merged 5 commits into from
Jun 12, 2022
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
5 changes: 5 additions & 0 deletions src.csharp/AlphaTab/Core/EcmaScript/Set.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,10 @@ IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}

public void Delete(T item)
{
_data.Remove(item);
}
}
}
16 changes: 15 additions & 1 deletion src.csharp/AlphaTab/Core/TypeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ public static void InsertRange<T>(this IList<T> data, int index, IEnumerable<T>
}
}

public static void Sort<T>(this IList<T> data, Func<T, T, double> func)
public static IList<T> Sort<T>(this IList<T> data, Func<T, T, double> func)
{
switch (data)
{
Expand All @@ -156,6 +156,8 @@ public static void Sort<T>(this IList<T> data, Func<T, T, double> func)
throw new NotSupportedException("Cannot sort list of type " +
data.GetType().FullName);
}

return data;
}
public static void Sort<T>(this IList<T> data)
{
Expand Down Expand Up @@ -314,6 +316,11 @@ public static bool IsTruthy(object? s)
{
return s != null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsTruthy(bool? b)
{
return b.GetValueOrDefault();
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsTruthy(double s)
Expand All @@ -328,6 +335,13 @@ public static IList<TResult> Map<TSource, TResult>(this IList<TSource> source,
return source.Select(func).ToList();
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IList<double> Map<TSource>(this IList<TSource> source,
Func<TSource, int> func)
{
return source.Select(i => (double)func(i)).ToList();
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string SubstringIndex(this string s, double startIndex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ public class List<T> : Iterable<T> {
return _data.removeLast()
}

public fun sort(comparison: (a: T, b: T) -> Double) {
public fun sort(comparison: (a: T, b: T) -> Double) : List<T> {
_data.sortWith { a, b -> comparison(a, b).toInt() }
return this
}

public fun <TOut> map(transform: (v: T) -> TOut): List<TOut> {
Expand All @@ -75,6 +76,15 @@ public class List<T> : Iterable<T> {
return mapped
}

@kotlin.jvm.JvmName("mapIntToDouble")
public fun map(transform: (v: T) -> Int): DoubleList {
val mapped = DoubleList(_data.size)
_data.forEachIndexed { index, item ->
mapped[index] = transform(item).toDouble()
}
return mapped
}

public fun reverse(): List<T> {
_data.reverse()
return this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ internal class Set<T> : Iterable<T> {
return _set.contains(item)
}

public fun delete(item: T) {
_set.remove(item)
}

public fun forEach(action: (item: T) -> Unit) {
for (i in _set) {
action(i)
Expand Down
10 changes: 8 additions & 2 deletions src/importer/MusicXmlImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ export class MusicXmlImporter extends ScoreImporter {
this.mergePartGroups();
}
this._score.finish(this.settings);
// the structure of MusicXML does not allow live creation of the groups,
this._score.rebuildRepeatGroups();
return this._score;
}
Expand Down Expand Up @@ -848,6 +847,13 @@ export class MusicXmlImporter extends ScoreImporter {
if (!slurNumber) {
slurNumber = '1';
}

// slur numbers are unique in the way that they have the same ID across
// staffs/tracks etc. as long they represent the logically same slur.
// but in our case it must be globally unique to link the correct notes.
// adding the staff ID should be enough to achieve this
slurNumber = beat.voice.bar.staff.index + '_' + slurNumber;

switch (c.getAttribute('type')) {
case 'start':
this._slurStarts.set(slurNumber, note);
Expand All @@ -857,7 +863,7 @@ export class MusicXmlImporter extends ScoreImporter {
note.isSlurDestination = true;
let slurStart: Note = this._slurStarts.get(slurNumber)!;
slurStart.slurDestination = note;
note.slurOrigin = note;
note.slurOrigin = slurStart;
}
break;
}
Expand Down
111 changes: 80 additions & 31 deletions src/midi/MidiPlaybackController.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
import { RepeatGroup } from '@src/model';
import { MasterBar } from '@src/model/MasterBar';
import { Score } from '@src/model/Score';

/**
* Helper container to handle repeats correctly
*/
class Repeat {
public group: RepeatGroup;
public opening: MasterBar;
public iterations: number[];
public closingIndex: number = 0;

public constructor(group: RepeatGroup, opening: MasterBar) {
this.group = group;
this.opening = opening;
// sort ascending according to index
group.closings = group.closings.sort((a, b) => a.index - b.index);
this.iterations = group.closings.map(_ => 0);
}
}

export class MidiPlaybackController {
private _score: Score;
private _repeatStartIndex: number = 0;
private _repeatNumber: number = 0;
private _repeatOpen: boolean = false;

private _repeatStack: Repeat[] = [];
private _groupsOnStack: Set<RepeatGroup> = new Set<RepeatGroup>();
private _previousAlternateEndings: number = 0;
public shouldPlay: boolean = true;
public index: number = 0;
public currentTick: number = 0;
Expand All @@ -21,31 +40,40 @@ export class MidiPlaybackController {

public processCurrent(): void {
const masterBar: MasterBar = this._score.masterBars[this.index];
const masterBarAlternateEndings: number = masterBar.alternateEndings;
// if the repeat group wasn't closed we reset the repeating
// on the last group opening
if (
!masterBar.repeatGroup.isClosed &&
masterBar.repeatGroup.openings[masterBar.repeatGroup.openings.length - 1] === masterBar
) {
this._repeatNumber = 0;
this._repeatOpen = false;
let masterBarAlternateEndings: number = masterBar.alternateEndings;
// if there are no alternate endings set on this bar. take the ones
// from the previously played bar which had alternate endings
if(masterBarAlternateEndings === 0) {
masterBarAlternateEndings = this._previousAlternateEndings;
}
if ((masterBar.isRepeatStart || masterBar.index === 0) && this._repeatNumber === 0) {
this._repeatStartIndex = this.index;
this._repeatOpen = true;
} else if (masterBar.isRepeatStart) {
this.shouldPlay = true;

// Repeat start (only properly closed ones)
if (masterBar === masterBar.repeatGroup.opening && masterBar.repeatGroup.isClosed) {
// first encounter of the repeat group? -> initialize repeats accordingly
if (!this._groupsOnStack.has(masterBar.repeatGroup)) {
const repeat = new Repeat(masterBar.repeatGroup, masterBar);
this._repeatStack.push(repeat);
this._groupsOnStack.add(masterBar.repeatGroup);
this._previousAlternateEndings = 0;
}
}
// if we encounter an alternate ending
if (this._repeatOpen && masterBarAlternateEndings > 0) {

// if we're not within repeats or not alternative endings set -> simply play
if (this._repeatStack.length === 0 || masterBarAlternateEndings === 0) {
this.shouldPlay = true;
} else {
const repeat = this._repeatStack[this._repeatStack.length - 1];
const iteration = repeat.iterations[repeat.closingIndex];
this._previousAlternateEndings = masterBarAlternateEndings;

// do we need to skip this section?
if ((masterBarAlternateEndings & (1 << this._repeatNumber)) === 0) {
if ((masterBarAlternateEndings & (1 << iteration)) === 0) {
this.shouldPlay = false;
} else {
this.shouldPlay = true;
}
}

if (this.shouldPlay) {
this.currentTick += masterBar.calculateDuration();
}
Expand All @@ -54,21 +82,42 @@ export class MidiPlaybackController {
public moveNext(): void {
const masterBar: MasterBar = this._score.masterBars[this.index];
const masterBarRepeatCount: number = masterBar.repeatCount - 1;
// if we encounter a repeat end
if (this._repeatOpen && masterBarRepeatCount > 0) {
// more repeats required?
if (this._repeatNumber < masterBarRepeatCount) {
// if we encounter a repeat end...
if (this._repeatStack.length > 0 && masterBarRepeatCount > 0) {
// ...more repeats required?
const repeat = this._repeatStack[this._repeatStack.length - 1];
const iteration = repeat.iterations[repeat.closingIndex];

// -> if yes, increase the iteration and jump back to start
if (iteration < masterBarRepeatCount) {
// jump to start
this.index = this._repeatStartIndex;
this._repeatNumber++;
this.index = repeat.opening.index;
repeat.iterations[repeat.closingIndex]++;

// clear iterations for previous closings and start over all repeats
// this ensures on scenarios like "open, bar, close, bar, close"
// that the second close will repeat again the first repeat.
for (let i = 0; i < repeat.closingIndex; i++) {
repeat.iterations[i] = 0;
}
repeat.closingIndex = 0;
this._previousAlternateEndings = 0;
} else {
// no repeats anymore, jump after repeat end
this._repeatNumber = 0;
this._repeatOpen = false;
this.shouldPlay = true;
this.index++;
// if we don't have further iterations left but we have additional closings in this group
// proceed heading to the next close but keep the repeat group active
if (repeat.closingIndex < repeat.group.closings.length - 1) {
repeat.closingIndex++;
this.index++; // go to next bar after current close
} else {
// if there are no further closings in the current group, we consider the current repeat done and handled
this._repeatStack.pop();
this._groupsOnStack.delete(repeat.group);

this.index++; // go to next bar after current close
}
}
} else {
// we have no started repeat, just proceed to next bar
this.index++;
}
}
Expand Down
5 changes: 2 additions & 3 deletions src/model/MasterBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,8 @@ export class MasterBar {
let duration: number = 0;
for (let track of this.score.tracks) {
for (let staff of track.staves) {
let barDuration: number = this.index < staff.bars.length
? staff.bars[this.index].calculateDuration()
: 0;
let barDuration: number =
this.index < staff.bars.length ? staff.bars[this.index].calculateDuration() : 0;
if (barDuration > duration) {
duration = barDuration;
}
Expand Down
26 changes: 14 additions & 12 deletions src/model/RepeatGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,44 @@ export class RepeatGroup {
*/
public masterBars: MasterBar[] = [];

/**
* the masterbars which opens the group.
*/
public opening: MasterBar|null = null;

/**
* a list of masterbars which open the group.
* @deprecated There can only be one opening, use the opening property instead
*/
public openings: MasterBar[] = [];
public get openings(): MasterBar[] {
const opening = this.opening;
return opening ? [opening] : [];
}

/**
* a list of masterbars which close the group.
*/
public closings: MasterBar[] = [];

/**
* true if the repeat group was opened well
* Gets whether this repeat group is really opened as a repeat.
*/
public isOpened: boolean = false;
public get isOpened():boolean { return this.opening?.isRepeatStart === true; }

/**
* true if the repeat group was closed well
*/
public isClosed: boolean = false;

public addMasterBar(masterBar: MasterBar): void {
if (this.openings.length === 0) {
this.openings.push(masterBar);
if (this.opening === null) {
this.opening = masterBar;
}
this.masterBars.push(masterBar);
masterBar.repeatGroup = this;
if (masterBar.isRepeatEnd) {
this.closings.push(masterBar);
this.isClosed = true;
if (!this.isOpened) {
this.masterBars[0].isRepeatStart = true;
this.isOpened = true;
}
} else if (this.isClosed) {
this.isClosed = false;
this.openings.push(masterBar);
}
}
}
Loading