Skip to content

Commit 65f6d93

Browse files
authored
Add support for full-bar rests (#503)
1 parent dee694d commit 65f6d93

7 files changed

+222
-68
lines changed

src/importer/CapellaParser.ts

+16-41
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import { Logger } from '@src/alphatab';
2525
import { Fermata, FermataType } from '@src/model/Fermata';
2626
import { DynamicValue } from '@src/model/DynamicValue';
2727
import { Ottavia } from '@src/model/Ottavia';
28-
import { MidiUtils } from '@src/midi/MidiUtils';
2928
import { KeySignature } from '@src/model/KeySignature';
3029

3130
class DrawObject {
@@ -54,9 +53,9 @@ class GuitarDrawObject extends DrawObject {
5453
public chord: Chord = new Chord();
5554
}
5655

57-
class SlurDrawObject extends DrawObject {}
56+
class SlurDrawObject extends DrawObject { }
5857

59-
class WavyLineDrawObject extends DrawObject {}
58+
class WavyLineDrawObject extends DrawObject { }
6059

6160
class TupletBracketDrawObject extends DrawObject {
6261
public number: number = 0;
@@ -76,7 +75,7 @@ class OctaveClefDrawObject extends DrawObject {
7675
public octave: number = 1;
7776
}
7877

79-
class TrillDrawObject extends DrawObject {}
78+
class TrillDrawObject extends DrawObject { }
8079

8180
class StaffLayout {
8281
public defaultClef: Clef = Clef.G2;
@@ -670,7 +669,7 @@ export class CapellaParser {
670669
break;
671670
case 'repEnd':
672671
this._currentVoiceState.repeatEnd = this._currentBar.masterBar;
673-
if(this._currentBar.masterBar.repeatCount < this._currentVoiceState.repeatCount) {
672+
if (this._currentBar.masterBar.repeatCount < this._currentVoiceState.repeatCount) {
674673
this._currentBar.masterBar.repeatCount = this._currentVoiceState.repeatCount;
675674
}
676675
this.parseBarDrawObject(c);
@@ -687,7 +686,7 @@ export class CapellaParser {
687686
break;
688687
case 'repEndBegin':
689688
this._currentVoiceState.repeatEnd = this._currentBar.masterBar;
690-
if(this._currentBar.masterBar.repeatCount < this._currentVoiceState.repeatCount) {
689+
if (this._currentBar.masterBar.repeatCount < this._currentVoiceState.repeatCount) {
691690
this._currentBar.masterBar.repeatCount = this._currentVoiceState.repeatCount;
692691
}
693692
this.parseBarDrawObject(c);
@@ -728,11 +727,11 @@ export class CapellaParser {
728727
}
729728
break;
730729
case 'rest':
731-
const restBeats = this.parseRestDurations(
730+
const restBeat = this.parseRestDurations(
732731
this._currentBar,
733732
c.findChildElement('duration')!
734733
);
735-
for (const restBeat of restBeats) {
734+
if (restBeat) {
736735
this.initFromPreviousBeat(restBeat, this._currentVoice);
737736
restBeat.updateDurations();
738737
this._currentVoiceState.currentPosition += restBeat.playbackDuration;
@@ -980,7 +979,7 @@ export class CapellaParser {
980979
private applyVolta(obj: VoltaDrawObject) {
981980
if (obj.lastNumber > 0) {
982981
this._currentVoiceState.repeatCount = obj.lastNumber;
983-
if (this._currentVoiceState.repeatEnd &&
982+
if (this._currentVoiceState.repeatEnd &&
984983
this._currentVoiceState.repeatEnd.repeatCount < this._currentVoiceState.repeatCount) {
985984
this._currentVoiceState.repeatEnd.repeatCount = this._currentVoiceState.repeatCount;
986985
}
@@ -1000,7 +999,7 @@ export class CapellaParser {
1000999
this._currentBar.masterBar.alternateEndings = alternateEndings;
10011000
} else if (obj.lastNumber > 0) {
10021001
this._currentBar.masterBar.alternateEndings = 0x01 << (obj.lastNumber - 1);
1003-
} else if(obj.firstNumber > 0) {
1002+
} else if (obj.firstNumber > 0) {
10041003
this._currentBar.masterBar.alternateEndings = 0x01 << (obj.firstNumber - 1);
10051004
}
10061005
}
@@ -1024,50 +1023,26 @@ export class CapellaParser {
10241023
}
10251024
}
10261025

1027-
private parseRestDurations(bar: Bar, element: XmlNode): Beat[] {
1026+
private parseRestDurations(bar: Bar, element: XmlNode): Beat | null {
10281027
const durationBase = element.getAttribute('base');
10291028
if (durationBase.indexOf('/') !== -1) {
10301029
let restBeat = new Beat();
10311030
restBeat.beamingMode = this._beamingMode;
10321031
this.parseDuration(bar, restBeat, element);
1033-
return [restBeat];
1032+
return restBeat;
10341033
}
10351034

10361035
// for
10371036
const fullBars = parseInt(durationBase);
10381037
if (fullBars === 1) {
1039-
let restBeats: Beat[] = [];
1040-
let remainingTicks = bar.masterBar.calculateDuration(false) * fullBars;
1041-
let currentRestDuration = Duration.Whole;
1042-
let currentRestDurationTicks = MidiUtils.toTicks(currentRestDuration);
1043-
while (remainingTicks > 0) {
1044-
// reduce to the duration that fits into the remaining time
1045-
while (
1046-
currentRestDurationTicks > remainingTicks &&
1047-
currentRestDuration < Duration.TwoHundredFiftySixth
1048-
) {
1049-
currentRestDuration = ((currentRestDuration as number) * 2) as Duration;
1050-
currentRestDurationTicks = MidiUtils.toTicks(currentRestDuration);
1051-
}
1052-
1053-
// no duration will fit anymore
1054-
if (currentRestDurationTicks > remainingTicks) {
1055-
break;
1056-
}
1057-
1058-
let restBeat = new Beat();
1059-
restBeat.beamingMode = this._beamingMode;
1060-
restBeat.duration = currentRestDuration;
1061-
restBeats.push(restBeat);
1062-
1063-
remainingTicks -= currentRestDurationTicks;
1064-
}
1065-
1066-
return restBeats;
1038+
let restBeat = new Beat();
1039+
restBeat.beamingMode = this._beamingMode;
1040+
restBeat.duration = Duration.Whole;
1041+
return restBeat;
10671042
} else {
10681043
// TODO: multibar rests
10691044
Logger.warning('Importer', `Multi-Bar rests are not supported`);
1070-
return [];
1045+
return null;
10711046
}
10721047
}
10731048

src/importer/MusicXmlImporter.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,7 @@ export class MusicXmlImporter extends ScoreImporter {
614614
beat.isEmpty = false;
615615
beat.addNote(note);
616616
beat.dots = 0;
617+
let isFullBarRest = false;
617618
for (let c of element.childNodes) {
618619
if (c.nodeType === XmlNodeType.Element) {
619620
switch (c.localName) {
@@ -626,7 +627,7 @@ export class MusicXmlImporter extends ScoreImporter {
626627
beat.duration = Duration.ThirtySecond;
627628
break;
628629
case 'duration':
629-
if (beat.isRest) {
630+
if (beat.isRest && !isFullBarRest) {
630631
// unit: divisions per quarter note
631632
let duration: number = parseInt(c.innerText);
632633
switch (duration) {
@@ -708,8 +709,10 @@ export class MusicXmlImporter extends ScoreImporter {
708709
this.parseUnpitched(c, note);
709710
break;
710711
case 'rest':
712+
isFullBarRest = c.getAttribute('measure') === 'yes';
711713
beat.isEmpty = false;
712714
beat.notes = [];
715+
beat.duration = Duration.Whole;
713716
break;
714717
}
715718
}

src/model/Beat.ts

+10
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,13 @@ export class Beat {
177177
return this.isEmpty || this.notes.length === 0;
178178
}
179179

180+
/**
181+
* Gets a value indicating whether this beat is a full bar rest.
182+
*/
183+
public get isFullBarRest(): boolean {
184+
return this.isRest && this.voice.beats.length === 1 && this.duration === Duration.Whole;
185+
}
186+
180187
/**
181188
* Gets or sets whether any note in this beat has a let-ring applied.
182189
* @json_ignore
@@ -509,6 +516,9 @@ export class Beat {
509516
}
510517

511518
private calculateDuration(): number {
519+
if(this.isFullBarRest) {
520+
return this.voice.bar.masterBar.calculateDuration();
521+
}
512522
let ticks: number = MidiUtils.toTicks(this.duration);
513523
if (this.dots === 2) {
514524
ticks = MidiUtils.applyDot(ticks, true);
+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
3+
<score-partwise version="3.1">
4+
<work>
5+
<work-title>Title</work-title>
6+
</work>
7+
<identification>
8+
<creator type="composer">Composer</creator>
9+
<encoding>
10+
<software>MuseScore 3.5.0</software>
11+
<encoding-date>2021-01-07</encoding-date>
12+
<supports element="accidental" type="yes"/>
13+
<supports element="beam" type="yes"/>
14+
<supports element="print" attribute="new-page" type="yes" value="yes"/>
15+
<supports element="print" attribute="new-system" type="yes" value="yes"/>
16+
<supports element="stem" type="yes"/>
17+
</encoding>
18+
</identification>
19+
<defaults>
20+
<scaling>
21+
<millimeters>7.05556</millimeters>
22+
<tenths>40</tenths>
23+
</scaling>
24+
<page-layout>
25+
<page-height>1683.36</page-height>
26+
<page-width>1190.88</page-width>
27+
<page-margins type="even">
28+
<left-margin>56.6929</left-margin>
29+
<right-margin>56.6929</right-margin>
30+
<top-margin>56.6929</top-margin>
31+
<bottom-margin>113.386</bottom-margin>
32+
</page-margins>
33+
<page-margins type="odd">
34+
<left-margin>56.6929</left-margin>
35+
<right-margin>56.6929</right-margin>
36+
<top-margin>56.6929</top-margin>
37+
<bottom-margin>113.386</bottom-margin>
38+
</page-margins>
39+
</page-layout>
40+
<word-font font-family="FreeSerif" font-size="10"/>
41+
<lyric-font font-family="FreeSerif" font-size="11"/>
42+
</defaults>
43+
<credit page="1">
44+
<credit-words default-x="595.44" default-y="1626.67" justify="center" valign="top" font-size="24">Title</credit-words>
45+
</credit>
46+
<credit page="1">
47+
<credit-words default-x="1134.19" default-y="1526.67" justify="right" valign="bottom" font-size="12">Composer</credit-words>
48+
</credit>
49+
<part-list>
50+
<score-part id="P1">
51+
<part-name>Piano</part-name>
52+
<part-abbreviation>Pno.</part-abbreviation>
53+
<score-instrument id="P1-I1">
54+
<instrument-name>Piano</instrument-name>
55+
</score-instrument>
56+
<midi-device id="P1-I1" port="1"></midi-device>
57+
<midi-instrument id="P1-I1">
58+
<midi-channel>1</midi-channel>
59+
<midi-program>1</midi-program>
60+
<volume>78.7402</volume>
61+
<pan>0</pan>
62+
</midi-instrument>
63+
</score-part>
64+
</part-list>
65+
<part id="P1">
66+
<measure number="1" width="179.49">
67+
<print>
68+
<system-layout>
69+
<system-margins>
70+
<left-margin>0.00</left-margin>
71+
<right-margin>650.76</right-margin>
72+
</system-margins>
73+
<top-system-distance>170.00</top-system-distance>
74+
</system-layout>
75+
</print>
76+
<attributes>
77+
<divisions>1</divisions>
78+
<key>
79+
<fifths>0</fifths>
80+
</key>
81+
<time>
82+
<beats>2</beats>
83+
<beat-type>4</beat-type>
84+
</time>
85+
<clef>
86+
<sign>G</sign>
87+
<line>2</line>
88+
</clef>
89+
</attributes>
90+
<note>
91+
<rest measure="yes"/>
92+
<duration>2</duration>
93+
<voice>1</voice>
94+
</note>
95+
</measure>
96+
<measure number="2" width="123.62">
97+
<note>
98+
<rest measure="yes"/>
99+
<duration>2</duration>
100+
<voice>1</voice>
101+
</note>
102+
</measure>
103+
<measure number="3" width="123.62">
104+
<note>
105+
<rest measure="yes"/>
106+
<duration>2</duration>
107+
<voice>1</voice>
108+
</note>
109+
<barline location="right">
110+
<bar-style>light-heavy</bar-style>
111+
</barline>
112+
</measure>
113+
</part>
114+
</score-partwise>

test/audio/MidiFileGenerator.test.ts

+39-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import {
2424
ProgramChangeEvent,
2525
TempoEvent,
2626
TimeSignatureEvent,
27-
TrackEndEvent
27+
TrackEndEvent,
28+
RestEvent
2829
} from '@test/audio/FlatMidiEventGenerator';
2930
import { TestPlatform } from '@test/TestPlatform';
3031

@@ -798,4 +799,41 @@ describe('MidiFileGeneratorTest', () => {
798799
expect(handler.midiEvents.length).toEqual(expectedEvents.length);
799800
});
800801

802+
it('full-bar-rest', () => {
803+
let tex: string = '\\ts 3 4 3.3.4 3.3.4 3.3.4 | r.1 | 3.3.4 3.3.4 3.3.4';
804+
let score: Score = parseTex(tex);
805+
expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].isFullBarRest).toBeTrue();
806+
807+
let expectedNoteOnTimes:number[] = [
808+
0 * MidiUtils.QuarterTime, // note 1
809+
1 * MidiUtils.QuarterTime, // note 2
810+
2 * MidiUtils.QuarterTime, // note 3
811+
3 * MidiUtils.QuarterTime, // 3/4 rest
812+
6 * MidiUtils.QuarterTime, // note 4
813+
7 * MidiUtils.QuarterTime, // note 5
814+
8 * MidiUtils.QuarterTime, // note 6
815+
];
816+
let noteOnTimes:number[] = [];
817+
let beat: Beat | null = score.tracks[0].staves[0].bars[0].voices[0].beats[0];
818+
while (beat != null) {
819+
noteOnTimes.push(beat.absolutePlaybackStart);
820+
beat = beat.nextBeat;
821+
}
822+
823+
expect(noteOnTimes.join(',')).toEqual(expectedNoteOnTimes.join(','));
824+
825+
let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator();
826+
let generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler);
827+
generator.generate();
828+
noteOnTimes = [];
829+
for(const evt of handler.midiEvents) {
830+
if(evt instanceof NoteEvent) {
831+
noteOnTimes.push(evt.tick);
832+
} else if(evt instanceof RestEvent) {
833+
noteOnTimes.push(evt.tick);
834+
}
835+
}
836+
expect(noteOnTimes.join(',')).toEqual(expectedNoteOnTimes.join(','));
837+
});
838+
801839
});
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { MusicXmlImporterTestHelper } from '@test/importer/MusicXmlImporterTestHelper';
2+
import { Score } from '@src/model/Score';
3+
4+
describe('MusicXmlImporterTests', () => {
5+
it('track-volume', async () => {
6+
let score: Score = await MusicXmlImporterTestHelper.testReferenceFile(
7+
'test-data/musicxml3/track-volume-balance.musicxml'
8+
);
9+
10+
expect(score.tracks[0].playbackInfo.volume).toBe(16);
11+
expect(score.tracks[1].playbackInfo.volume).toBe(12);
12+
expect(score.tracks[2].playbackInfo.volume).toBe(8);
13+
expect(score.tracks[3].playbackInfo.volume).toBe(4);
14+
expect(score.tracks[4].playbackInfo.volume).toBe(0);
15+
});
16+
17+
it('track-balance', async () => {
18+
let score: Score = await MusicXmlImporterTestHelper.testReferenceFile(
19+
'test-data/musicxml3/track-volume-balance.musicxml'
20+
);
21+
22+
expect(score.tracks[0].playbackInfo.balance).toBe(0);
23+
expect(score.tracks[1].playbackInfo.balance).toBe(4);
24+
expect(score.tracks[2].playbackInfo.balance).toBe(8);
25+
expect(score.tracks[3].playbackInfo.balance).toBe(12);
26+
expect(score.tracks[4].playbackInfo.balance).toBe(16);
27+
});
28+
29+
30+
it('full-bar-rest', async () => {
31+
let score: Score = await MusicXmlImporterTestHelper.testReferenceFile(
32+
'test-data/musicxml3/full-bar-rest.musicxml'
33+
);
34+
35+
expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].isFullBarRest).toBeTrue();
36+
expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].isFullBarRest).toBeTrue();
37+
expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].isFullBarRest).toBeTrue();
38+
});
39+
});

0 commit comments

Comments
 (0)