Skip to content

Commit 7204510

Browse files
authored
docs(releases): finish the 1.9.0 notes, load real MusicXML and Capella demos, fix Capella import (#3002)
1 parent 54d916b commit 7204510

10 files changed

Lines changed: 306 additions & 52 deletions

File tree

‎packages/alphatab/src/importer/CapellaParser.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ export class CapellaParser {
550550
}
551551

552552
private _voiceStates: Map<string, CapellaVoiceState> = new Map();
553-
private _currentVoiceState!: CapellaVoiceState;
553+
private _currentVoiceState: CapellaVoiceState = new CapellaVoiceState();
554554
private _currentBar!: Bar;
555555
private _currentVoice!: Voice;
556556

@@ -572,7 +572,7 @@ export class CapellaParser {
572572
element: XmlNode
573573
) {
574574
const voiceStateKey = `${staffId}_${voiceIndex}`;
575-
if (this._currentVoiceState && !this._currentVoiceState.currentBarComplete) {
575+
if (!this._currentVoiceState.currentBarComplete) {
576576
this._currentBar.masterBar.isAnacrusis = true;
577577
}
578578

@@ -704,12 +704,14 @@ export class CapellaParser {
704704
chordBeat.preferredBeamDirection = this._currentVoiceState.voiceStemDir;
705705
}
706706
this._parseDuration(chordBeat, c.findChildElement('duration')!);
707-
chordBeat.updateDurations();
708-
this._currentVoiceState.currentPosition += chordBeat.playbackDuration;
709707
this._currentVoice.addBeat(chordBeat);
710708

711709
this._parseChord(chordBeat, c);
712710

711+
// the duration depends on the voice and the notes (full-bar rest detection)
712+
chordBeat.updateDurations();
713+
this._currentVoiceState.currentPosition += chordBeat.playbackDuration;
714+
713715
if (this._currentVoiceState.currentPosition >= this._currentVoiceState.currentBarDuration) {
714716
this._currentVoiceState.currentBarComplete = true;
715717
}
@@ -718,9 +720,9 @@ export class CapellaParser {
718720
const restBeat = this._parseRestDurations(c.findChildElement('duration')!);
719721
if (restBeat) {
720722
this._initFromPreviousBeat(restBeat, this._currentVoice);
723+
this._currentVoice.addBeat(restBeat);
721724
restBeat.updateDurations();
722725
this._currentVoiceState.currentPosition += restBeat.playbackDuration;
723-
this._currentVoice.addBeat(restBeat);
724726

725727
if (this._currentVoiceState.currentPosition >= this._currentVoiceState.currentBarDuration) {
726728
this._currentVoiceState.currentBarComplete = true;
1.14 KB
Binary file not shown.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader';
2+
import { IOHelper } from '@coderline/alphatab/io/IOHelper';
3+
import { Clef } from '@coderline/alphatab/model/Clef';
4+
import { Duration } from '@coderline/alphatab/model/Duration';
5+
import type { Score } from '@coderline/alphatab/model/Score';
6+
import type { Staff } from '@coderline/alphatab/model/Staff';
7+
import { Settings } from '@coderline/alphatab/Settings';
8+
import { TestPlatform } from 'test/TestPlatform';
9+
import { describe, expect, it } from 'vitest';
10+
11+
describe('CapellaImporterTests', () => {
12+
function pitches(staff: Staff): number[][] {
13+
return staff.bars.map(b => b.voices[0].beats.map(beat => (beat.isRest ? -1 : beat.notes[0].realValue)));
14+
}
15+
16+
function capXml(noteObjects: string): Score {
17+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
18+
<score xmlns="http://www.capella.de/CapXML/3.0">
19+
<info/>
20+
<layout>
21+
<staves>
22+
<staffLayout description="Voice"><notation defaultClef="treble"/><sound instr="0" volume="100"/></staffLayout>
23+
</staves>
24+
</layout>
25+
<systems>
26+
<system>
27+
<staves>
28+
<staff layout="Voice" defaultTime="4/4">
29+
<voices><voice><noteObjects>${noteObjects}</noteObjects></voice></voices>
30+
</staff>
31+
</staves>
32+
</system>
33+
</systems>
34+
</score>`;
35+
return ScoreLoader.loadScoreFromBytes(IOHelper.stringToBytes(xml), new Settings());
36+
}
37+
38+
it('grand-staff', async () => {
39+
const data = await TestPlatform.loadFile('test-data/capella/twinkle-mozart.capx');
40+
const score = ScoreLoader.loadScoreFromBytes(data, new Settings());
41+
42+
expect(score.title).toBe('Twinkle, Twinkle Little Star');
43+
expect(score.artist).toBe('W.A. Mozart (1756-1791)');
44+
expect(score.tracks[0].name).toBe('Piano');
45+
expect(score.masterBars.length).toBe(4);
46+
expect(score.tempo).toBe(100);
47+
expect(score.tracks.length).toBe(1);
48+
49+
expect(score.tracks[0].staves.length).toBe(2);
50+
const rightHand = score.tracks[0].staves[0];
51+
const leftHand = score.tracks[0].staves[1];
52+
expect(rightHand.bars[0].clef).toBe(Clef.G2);
53+
expect(leftHand.bars[0].clef).toBe(Clef.F4);
54+
expect(pitches(rightHand)).toEqual([
55+
[60, 60, 67, 67],
56+
[69, 69, 67],
57+
[65, 65, 64, 64],
58+
[62, 62, 60]
59+
]);
60+
expect(pitches(leftHand)).toEqual([
61+
[48, 52, 55, 52],
62+
[53, 57, 55, 52],
63+
[50, 53, 48, 52],
64+
[43, 47, 48]
65+
]);
66+
expect(rightHand.bars[1].voices[0].beats[2].duration).toBe(Duration.Half);
67+
expect(rightHand.bars[0].voices[0].beats.map(b => b.lyrics![0])).toEqual(['Twin-', 'kle,', 'twin-', 'kle,']);
68+
});
69+
70+
it('whole-notes-and-rests', () => {
71+
const score = capXml(`
72+
<chord><duration base="1/1"/><heads><head pitch="C5"/></heads></chord>
73+
<barline type="single"/>
74+
<rest><duration base="1/1"/></rest>
75+
<barline type="single"/>
76+
<rest><duration base="1"/></rest>
77+
<barline type="single"/>
78+
<chord><duration base="1/2"/><heads><head pitch="E5"/></heads></chord>
79+
<rest><duration base="1/2"/></rest>
80+
<barline type="end"/>`);
81+
82+
const staff = score.tracks[0].staves[0];
83+
expect(score.masterBars.length).toBe(4);
84+
expect(pitches(staff)).toEqual([[60], [-1], [-1], [64, -1]]);
85+
expect(staff.bars[1].voices[0].beats[0].isFullBarRest).toBe(true);
86+
expect(staff.bars[2].voices[0].beats[0].isFullBarRest).toBe(true);
87+
expect(staff.bars[0].voices[0].beats[0].isFullBarRest).toBe(false);
88+
expect(score.masterBars.map(m => m.isAnacrusis)).toEqual([false, false, false, false]);
89+
});
90+
});

‎website/docs/features/import-load/capella.mdx‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,9 @@ import { AlphaTab } from '@site/src/components/AlphaTab';
1414

1515
Scores authored in Capella arrive as `.capx` archives — Capella's XML container format. alphaTab imports them so a Capella export opens in the same viewer as your Guitar Pro and MusicXML files, without a separate conversion step.
1616

17-
:::info
18-
The Capella importer routes through the same content-sniffing load path as every other format. The preview below loads a curated Guitar Pro fixture for parity with sibling pages; point `core.file` at a `.capx` file in your own project to exercise the Capella importer end-to-end.
19-
:::
17+
The preview below loads a `.capx` file with the theme of Mozart's variations K. 265 on a piano grand staff, with lyrics under the right hand:
2018

21-
<AlphaTab tex={false} file="/files/features/Chords.gp5" />
19+
<AlphaTab tex={false} file="/files/twinkle-mozart.capx" />
2220

2321
## Scope of the Capella importer
2422

0 commit comments

Comments
 (0)