Skip to content

Commit

Permalink
Add support for full-bar rests (CoderLine#503)
Browse files Browse the repository at this point in the history
  • Loading branch information
Danielku15 committed Jan 7, 2021
1 parent dee694d commit 65f6d93
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 68 deletions.
57 changes: 16 additions & 41 deletions src/importer/CapellaParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import { Logger } from '@src/alphatab';
import { Fermata, FermataType } from '@src/model/Fermata';
import { DynamicValue } from '@src/model/DynamicValue';
import { Ottavia } from '@src/model/Ottavia';
import { MidiUtils } from '@src/midi/MidiUtils';
import { KeySignature } from '@src/model/KeySignature';

class DrawObject {
Expand Down Expand Up @@ -54,9 +53,9 @@ class GuitarDrawObject extends DrawObject {
public chord: Chord = new Chord();
}

class SlurDrawObject extends DrawObject {}
class SlurDrawObject extends DrawObject { }

class WavyLineDrawObject extends DrawObject {}
class WavyLineDrawObject extends DrawObject { }

class TupletBracketDrawObject extends DrawObject {
public number: number = 0;
Expand All @@ -76,7 +75,7 @@ class OctaveClefDrawObject extends DrawObject {
public octave: number = 1;
}

class TrillDrawObject extends DrawObject {}
class TrillDrawObject extends DrawObject { }

class StaffLayout {
public defaultClef: Clef = Clef.G2;
Expand Down Expand Up @@ -670,7 +669,7 @@ export class CapellaParser {
break;
case 'repEnd':
this._currentVoiceState.repeatEnd = this._currentBar.masterBar;
if(this._currentBar.masterBar.repeatCount < this._currentVoiceState.repeatCount) {
if (this._currentBar.masterBar.repeatCount < this._currentVoiceState.repeatCount) {
this._currentBar.masterBar.repeatCount = this._currentVoiceState.repeatCount;
}
this.parseBarDrawObject(c);
Expand All @@ -687,7 +686,7 @@ export class CapellaParser {
break;
case 'repEndBegin':
this._currentVoiceState.repeatEnd = this._currentBar.masterBar;
if(this._currentBar.masterBar.repeatCount < this._currentVoiceState.repeatCount) {
if (this._currentBar.masterBar.repeatCount < this._currentVoiceState.repeatCount) {
this._currentBar.masterBar.repeatCount = this._currentVoiceState.repeatCount;
}
this.parseBarDrawObject(c);
Expand Down Expand Up @@ -728,11 +727,11 @@ export class CapellaParser {
}
break;
case 'rest':
const restBeats = this.parseRestDurations(
const restBeat = this.parseRestDurations(
this._currentBar,
c.findChildElement('duration')!
);
for (const restBeat of restBeats) {
if (restBeat) {
this.initFromPreviousBeat(restBeat, this._currentVoice);
restBeat.updateDurations();
this._currentVoiceState.currentPosition += restBeat.playbackDuration;
Expand Down Expand Up @@ -980,7 +979,7 @@ export class CapellaParser {
private applyVolta(obj: VoltaDrawObject) {
if (obj.lastNumber > 0) {
this._currentVoiceState.repeatCount = obj.lastNumber;
if (this._currentVoiceState.repeatEnd &&
if (this._currentVoiceState.repeatEnd &&
this._currentVoiceState.repeatEnd.repeatCount < this._currentVoiceState.repeatCount) {
this._currentVoiceState.repeatEnd.repeatCount = this._currentVoiceState.repeatCount;
}
Expand All @@ -1000,7 +999,7 @@ export class CapellaParser {
this._currentBar.masterBar.alternateEndings = alternateEndings;
} else if (obj.lastNumber > 0) {
this._currentBar.masterBar.alternateEndings = 0x01 << (obj.lastNumber - 1);
} else if(obj.firstNumber > 0) {
} else if (obj.firstNumber > 0) {
this._currentBar.masterBar.alternateEndings = 0x01 << (obj.firstNumber - 1);
}
}
Expand All @@ -1024,50 +1023,26 @@ export class CapellaParser {
}
}

private parseRestDurations(bar: Bar, element: XmlNode): Beat[] {
private parseRestDurations(bar: Bar, element: XmlNode): Beat | null {
const durationBase = element.getAttribute('base');
if (durationBase.indexOf('/') !== -1) {
let restBeat = new Beat();
restBeat.beamingMode = this._beamingMode;
this.parseDuration(bar, restBeat, element);
return [restBeat];
return restBeat;
}

// for
const fullBars = parseInt(durationBase);
if (fullBars === 1) {
let restBeats: Beat[] = [];
let remainingTicks = bar.masterBar.calculateDuration(false) * fullBars;
let currentRestDuration = Duration.Whole;
let currentRestDurationTicks = MidiUtils.toTicks(currentRestDuration);
while (remainingTicks > 0) {
// reduce to the duration that fits into the remaining time
while (
currentRestDurationTicks > remainingTicks &&
currentRestDuration < Duration.TwoHundredFiftySixth
) {
currentRestDuration = ((currentRestDuration as number) * 2) as Duration;
currentRestDurationTicks = MidiUtils.toTicks(currentRestDuration);
}

// no duration will fit anymore
if (currentRestDurationTicks > remainingTicks) {
break;
}

let restBeat = new Beat();
restBeat.beamingMode = this._beamingMode;
restBeat.duration = currentRestDuration;
restBeats.push(restBeat);

remainingTicks -= currentRestDurationTicks;
}

return restBeats;
let restBeat = new Beat();
restBeat.beamingMode = this._beamingMode;
restBeat.duration = Duration.Whole;
return restBeat;
} else {
// TODO: multibar rests
Logger.warning('Importer', `Multi-Bar rests are not supported`);
return [];
return null;
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/importer/MusicXmlImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,7 @@ export class MusicXmlImporter extends ScoreImporter {
beat.isEmpty = false;
beat.addNote(note);
beat.dots = 0;
let isFullBarRest = false;
for (let c of element.childNodes) {
if (c.nodeType === XmlNodeType.Element) {
switch (c.localName) {
Expand All @@ -626,7 +627,7 @@ export class MusicXmlImporter extends ScoreImporter {
beat.duration = Duration.ThirtySecond;
break;
case 'duration':
if (beat.isRest) {
if (beat.isRest && !isFullBarRest) {
// unit: divisions per quarter note
let duration: number = parseInt(c.innerText);
switch (duration) {
Expand Down Expand Up @@ -708,8 +709,10 @@ export class MusicXmlImporter extends ScoreImporter {
this.parseUnpitched(c, note);
break;
case 'rest':
isFullBarRest = c.getAttribute('measure') === 'yes';
beat.isEmpty = false;
beat.notes = [];
beat.duration = Duration.Whole;
break;
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/model/Beat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,13 @@ export class Beat {
return this.isEmpty || this.notes.length === 0;
}

/**
* Gets a value indicating whether this beat is a full bar rest.
*/
public get isFullBarRest(): boolean {
return this.isRest && this.voice.beats.length === 1 && this.duration === Duration.Whole;
}

/**
* Gets or sets whether any note in this beat has a let-ring applied.
* @json_ignore
Expand Down Expand Up @@ -509,6 +516,9 @@ export class Beat {
}

private calculateDuration(): number {
if(this.isFullBarRest) {
return this.voice.bar.masterBar.calculateDuration();
}
let ticks: number = MidiUtils.toTicks(this.duration);
if (this.dots === 2) {
ticks = MidiUtils.applyDot(ticks, true);
Expand Down
114 changes: 114 additions & 0 deletions test-data/musicxml3/full-bar-rest.musicxml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 3.1 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">
<score-partwise version="3.1">
<work>
<work-title>Title</work-title>
</work>
<identification>
<creator type="composer">Composer</creator>
<encoding>
<software>MuseScore 3.5.0</software>
<encoding-date>2021-01-07</encoding-date>
<supports element="accidental" type="yes"/>
<supports element="beam" type="yes"/>
<supports element="print" attribute="new-page" type="yes" value="yes"/>
<supports element="print" attribute="new-system" type="yes" value="yes"/>
<supports element="stem" type="yes"/>
</encoding>
</identification>
<defaults>
<scaling>
<millimeters>7.05556</millimeters>
<tenths>40</tenths>
</scaling>
<page-layout>
<page-height>1683.36</page-height>
<page-width>1190.88</page-width>
<page-margins type="even">
<left-margin>56.6929</left-margin>
<right-margin>56.6929</right-margin>
<top-margin>56.6929</top-margin>
<bottom-margin>113.386</bottom-margin>
</page-margins>
<page-margins type="odd">
<left-margin>56.6929</left-margin>
<right-margin>56.6929</right-margin>
<top-margin>56.6929</top-margin>
<bottom-margin>113.386</bottom-margin>
</page-margins>
</page-layout>
<word-font font-family="FreeSerif" font-size="10"/>
<lyric-font font-family="FreeSerif" font-size="11"/>
</defaults>
<credit page="1">
<credit-words default-x="595.44" default-y="1626.67" justify="center" valign="top" font-size="24">Title</credit-words>
</credit>
<credit page="1">
<credit-words default-x="1134.19" default-y="1526.67" justify="right" valign="bottom" font-size="12">Composer</credit-words>
</credit>
<part-list>
<score-part id="P1">
<part-name>Piano</part-name>
<part-abbreviation>Pno.</part-abbreviation>
<score-instrument id="P1-I1">
<instrument-name>Piano</instrument-name>
</score-instrument>
<midi-device id="P1-I1" port="1"></midi-device>
<midi-instrument id="P1-I1">
<midi-channel>1</midi-channel>
<midi-program>1</midi-program>
<volume>78.7402</volume>
<pan>0</pan>
</midi-instrument>
</score-part>
</part-list>
<part id="P1">
<measure number="1" width="179.49">
<print>
<system-layout>
<system-margins>
<left-margin>0.00</left-margin>
<right-margin>650.76</right-margin>
</system-margins>
<top-system-distance>170.00</top-system-distance>
</system-layout>
</print>
<attributes>
<divisions>1</divisions>
<key>
<fifths>0</fifths>
</key>
<time>
<beats>2</beats>
<beat-type>4</beat-type>
</time>
<clef>
<sign>G</sign>
<line>2</line>
</clef>
</attributes>
<note>
<rest measure="yes"/>
<duration>2</duration>
<voice>1</voice>
</note>
</measure>
<measure number="2" width="123.62">
<note>
<rest measure="yes"/>
<duration>2</duration>
<voice>1</voice>
</note>
</measure>
<measure number="3" width="123.62">
<note>
<rest measure="yes"/>
<duration>2</duration>
<voice>1</voice>
</note>
<barline location="right">
<bar-style>light-heavy</bar-style>
</barline>
</measure>
</part>
</score-partwise>
40 changes: 39 additions & 1 deletion test/audio/MidiFileGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import {
ProgramChangeEvent,
TempoEvent,
TimeSignatureEvent,
TrackEndEvent
TrackEndEvent,
RestEvent
} from '@test/audio/FlatMidiEventGenerator';
import { TestPlatform } from '@test/TestPlatform';

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

it('full-bar-rest', () => {
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';
let score: Score = parseTex(tex);
expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].isFullBarRest).toBeTrue();

let expectedNoteOnTimes:number[] = [
0 * MidiUtils.QuarterTime, // note 1
1 * MidiUtils.QuarterTime, // note 2
2 * MidiUtils.QuarterTime, // note 3
3 * MidiUtils.QuarterTime, // 3/4 rest
6 * MidiUtils.QuarterTime, // note 4
7 * MidiUtils.QuarterTime, // note 5
8 * MidiUtils.QuarterTime, // note 6
];
let noteOnTimes:number[] = [];
let beat: Beat | null = score.tracks[0].staves[0].bars[0].voices[0].beats[0];
while (beat != null) {
noteOnTimes.push(beat.absolutePlaybackStart);
beat = beat.nextBeat;
}

expect(noteOnTimes.join(',')).toEqual(expectedNoteOnTimes.join(','));

let handler: FlatMidiEventGenerator = new FlatMidiEventGenerator();
let generator: MidiFileGenerator = new MidiFileGenerator(score, null, handler);
generator.generate();
noteOnTimes = [];
for(const evt of handler.midiEvents) {
if(evt instanceof NoteEvent) {
noteOnTimes.push(evt.tick);
} else if(evt instanceof RestEvent) {
noteOnTimes.push(evt.tick);
}
}
expect(noteOnTimes.join(',')).toEqual(expectedNoteOnTimes.join(','));
});

});
39 changes: 39 additions & 0 deletions test/importer/MusicXmlImporter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { MusicXmlImporterTestHelper } from '@test/importer/MusicXmlImporterTestHelper';
import { Score } from '@src/model/Score';

describe('MusicXmlImporterTests', () => {
it('track-volume', async () => {
let score: Score = await MusicXmlImporterTestHelper.testReferenceFile(
'test-data/musicxml3/track-volume-balance.musicxml'
);

expect(score.tracks[0].playbackInfo.volume).toBe(16);
expect(score.tracks[1].playbackInfo.volume).toBe(12);
expect(score.tracks[2].playbackInfo.volume).toBe(8);
expect(score.tracks[3].playbackInfo.volume).toBe(4);
expect(score.tracks[4].playbackInfo.volume).toBe(0);
});

it('track-balance', async () => {
let score: Score = await MusicXmlImporterTestHelper.testReferenceFile(
'test-data/musicxml3/track-volume-balance.musicxml'
);

expect(score.tracks[0].playbackInfo.balance).toBe(0);
expect(score.tracks[1].playbackInfo.balance).toBe(4);
expect(score.tracks[2].playbackInfo.balance).toBe(8);
expect(score.tracks[3].playbackInfo.balance).toBe(12);
expect(score.tracks[4].playbackInfo.balance).toBe(16);
});


it('full-bar-rest', async () => {
let score: Score = await MusicXmlImporterTestHelper.testReferenceFile(
'test-data/musicxml3/full-bar-rest.musicxml'
);

expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].isFullBarRest).toBeTrue();
expect(score.tracks[0].staves[0].bars[1].voices[0].beats[0].isFullBarRest).toBeTrue();
expect(score.tracks[0].staves[0].bars[2].voices[0].beats[0].isFullBarRest).toBeTrue();
});
});
Loading

0 comments on commit 65f6d93

Please sign in to comment.