diff --git a/src/importer/CapellaParser.ts b/src/importer/CapellaParser.ts index 01e17872e..e2ba111eb 100644 --- a/src/importer/CapellaParser.ts +++ b/src/importer/CapellaParser.ts @@ -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 { @@ -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; @@ -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; @@ -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); @@ -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); @@ -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; @@ -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; } @@ -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); } } @@ -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; } } diff --git a/src/importer/MusicXmlImporter.ts b/src/importer/MusicXmlImporter.ts index 73036bf76..5009d3360 100644 --- a/src/importer/MusicXmlImporter.ts +++ b/src/importer/MusicXmlImporter.ts @@ -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) { @@ -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) { @@ -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; } } diff --git a/src/model/Beat.ts b/src/model/Beat.ts index 779fdf8b3..f623b31d4 100644 --- a/src/model/Beat.ts +++ b/src/model/Beat.ts @@ -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 @@ -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); diff --git a/test-data/musicxml3/full-bar-rest.musicxml b/test-data/musicxml3/full-bar-rest.musicxml new file mode 100644 index 000000000..8537471fc --- /dev/null +++ b/test-data/musicxml3/full-bar-rest.musicxml @@ -0,0 +1,114 @@ + + + + + Title + + + Composer + + MuseScore 3.5.0 + 2021-01-07 + + + + + + + + + + 7.05556 + 40 + + + 1683.36 + 1190.88 + + 56.6929 + 56.6929 + 56.6929 + 113.386 + + + 56.6929 + 56.6929 + 56.6929 + 113.386 + + + + + + + Title + + + Composer + + + + Piano + Pno. + + Piano + + + + 1 + 1 + 78.7402 + 0 + + + + + + + + + 0.00 + 650.76 + + 170.00 + + + + 1 + + 0 + + + + G + 2 + + + + + 2 + 1 + + + + + + 2 + 1 + + + + + + 2 + 1 + + + light-heavy + + + + diff --git a/test/audio/MidiFileGenerator.test.ts b/test/audio/MidiFileGenerator.test.ts index cbe7794ca..24befbd2f 100644 --- a/test/audio/MidiFileGenerator.test.ts +++ b/test/audio/MidiFileGenerator.test.ts @@ -24,7 +24,8 @@ import { ProgramChangeEvent, TempoEvent, TimeSignatureEvent, - TrackEndEvent + TrackEndEvent, + RestEvent } from '@test/audio/FlatMidiEventGenerator'; import { TestPlatform } from '@test/TestPlatform'; @@ -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(',')); + }); + }); diff --git a/test/importer/MusicXmlImporter.test.ts b/test/importer/MusicXmlImporter.test.ts new file mode 100644 index 000000000..5cb8d83ac --- /dev/null +++ b/test/importer/MusicXmlImporter.test.ts @@ -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(); + }); +}); diff --git a/test/importer/MusicXmlImporterTestSuite.test.ts b/test/importer/MusicXmlImporterTestSuite.test.ts index 29ef946ca..4316c960f 100644 --- a/test/importer/MusicXmlImporterTestSuite.test.ts +++ b/test/importer/MusicXmlImporterTestSuite.test.ts @@ -1,5 +1,4 @@ import { MusicXmlImporterTestHelper } from '@test/importer/MusicXmlImporterTestHelper'; -import { Score } from '@src/model/Score'; describe('MusicXmlImporterTestSuiteTests', () => { it('01a_Pitches_Pitches', async () => { @@ -607,28 +606,4 @@ describe('MusicXmlImporterTestSuiteTests', () => { 'test-data/musicxml-testsuite/99b-Lyrics-BeamsMelismata-IgnoreBeams.xml' ); }); - - 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); - }); });