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

Add support for full-bar rests #503

Merged
merged 1 commit into from
Jan 7, 2021
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
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