Skip to content

Commit

Permalink
Merge pull request #202 from stringsync/fermata
Browse files Browse the repository at this point in the history
Render lilypond32a (fermatas)
  • Loading branch information
jaredjj3 authored Jan 19, 2024
2 parents 41a2e3a + bc61ea7 commit b01dcc3
Show file tree
Hide file tree
Showing 14 changed files with 197 additions and 6 deletions.
3 changes: 3 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const customSnapshotsDir =

const toMatchImageSnapshot = configureToMatchImageSnapshot({
customSnapshotsDir,
customDiffConfig: {
threshold: 0.01,
},
});

expect.extend({ toMatchImageSnapshot });
25 changes: 25 additions & 0 deletions src/musicxml/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,28 @@ export const SHOW_TUPLET = new Enum(['actual', 'both', 'none'] as const);
*/
export type TiedType = EnumValues<typeof TIED_TYPES>;
export const TIED_TYPES = new Enum(['start', 'stop', 'continue', 'let-ring'] as const);

/**
* The fermata-shape type represents the shape of the fermata sign.
*
* See https://www.w3.org/2021/06/musicxml40/musicxml-reference/data-types/fermata-shape/
*/
export type FermataShape = EnumValues<typeof FERMATA_SHAPES>;
export const FERMATA_SHAPES = new Enum([
'normal',
'angled',
'square',
'double-angled',
'double-square',
'double-dot',
'half-curve',
'curlew',
] as const);

/**
* The upright-inverted type describes the appearance of a fermata element.
*
* See https://www.w3.org/2021/06/musicxml40/musicxml-reference/data-types/upright-inverted/
*/
export type FermataType = EnumValues<typeof FERMATA_TYPES>;
export const FERMATA_TYPES = new Enum(['upright', 'inverted'] as const);
21 changes: 21 additions & 0 deletions src/musicxml/fermata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { NamedElement } from '@/util';
import { FERMATA_SHAPES, FERMATA_TYPES, FermataShape, FermataType } from './enums';

/**
* The `<fermata>` element content represents the shape of the fermata sign.
*
* See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/fermata/
*/
export class Fermata {
constructor(private element: NamedElement<'fermata'>) {}

/** Returns the shape of the fermata. Defaults to normal. */
getShape(): FermataShape {
return this.element.content().enum(FERMATA_SHAPES) ?? 'normal';
}

/** Returns the type of fermata. Defaults to upright. */
getType(): FermataType {
return this.element.attr('type').withDefault<FermataType>('upright').enum(FERMATA_TYPES);
}
}
1 change: 1 addition & 0 deletions src/musicxml/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './defaults';
export * from './direction';
export * from './directiontype';
export * from './enums';
export * from './fermata';
export * from './forward';
export * from './key';
export * from './lyric';
Expand Down
6 changes: 6 additions & 0 deletions src/musicxml/notations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Tuplet } from './tuplet';
import { Slur } from './slur';
import { Ornaments } from './ornaments';
import { Tied } from './tied';
import { Fermata } from './fermata';

/**
* Musical notations that apply to a specific note or chord.
Expand Down Expand Up @@ -47,4 +48,9 @@ export class Notations {
getOrnaments(): Ornaments[] {
return this.element.all('ornaments').map((element) => new Ornaments(element));
}

/** Returns the fermatas of the notations. Defaults to an empty array. */
getFermatas(): Fermata[] {
return this.element.all('fermata').map((element) => new Fermata(element));
}
}
39 changes: 39 additions & 0 deletions src/rendering/fermata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as vexflow from 'vexflow';
import * as musicxml from '@/musicxml';

/** The result of rendering an Articulation. */
export type FermataRendering = {
type: 'fermata';
vexflow: {
articulation: vexflow.Articulation;
};
};

/** Represents a Fermata. */
export class Fermata {
private musicXML: { fermata: musicxml.Fermata };

constructor(opts: { musicXML: { fermata: musicxml.Fermata } }) {
this.musicXML = opts.musicXML;
}

render(): FermataRendering {
return {
type: 'fermata',
vexflow: {
articulation: this.getVfArticulation(),
},
};
}

private getVfArticulation(): vexflow.Articulation {
const type = this.musicXML.fermata.getType();

switch (type) {
case 'upright':
return new vexflow.Articulation('a@a');
case 'inverted':
return new vexflow.Articulation('a@u').setPosition(vexflow.ModifierPosition.BELOW);
}
}
}
25 changes: 23 additions & 2 deletions src/rendering/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import * as conversions from './conversions';
import { Ornament, OrnamentRendering } from './ornament';
import { Spanners } from './spanners';
import { Address } from './address';
import { Fermata, FermataRendering } from './fermata';
import { fermata } from '../util/xml';

const STEP_ORDER = [
'Cb',
Expand All @@ -37,7 +39,12 @@ const STEP_ORDER = [
'B#',
];

export type NoteModifierRendering = AccidentalRendering | LyricRendering | TokenRendering | OrnamentRendering;
export type NoteModifierRendering =
| AccidentalRendering
| LyricRendering
| TokenRendering
| OrnamentRendering
| FermataRendering;

/** The result of rendering a Note. */
export type NoteRendering = StaveNoteRendering | GraceNoteRendering;
Expand Down Expand Up @@ -182,6 +189,10 @@ export class Note {
renderings.push(ornament.render());
}

for (const fermata of note.getFermatas()) {
renderings.push(fermata.render());
}

return renderings;
});

Expand All @@ -200,6 +211,9 @@ export class Note {
case 'ornament':
vfStaveNote.addModifier(modifierRendering.vexflow.ornament, index);
break;
case 'fermata':
vfStaveNote.addModifier(modifierRendering.vexflow.articulation, index);
break;
}
}
}
Expand Down Expand Up @@ -360,7 +374,14 @@ export class Note {
return this.musicXML.note
.getNotations()
.flatMap((notations) => notations.getOrnaments())
.flatMap((ornaments) => new Ornament({ musicXML: { ornaments } }));
.map((ornaments) => new Ornament({ musicXML: { ornaments } }));
}

private getFermatas(): Fermata[] {
return this.musicXML.note
.getNotations()
.flatMap((notations) => notations.getFermatas())
.map((fermata) => new Fermata({ musicXML: { fermata } }));
}

private getDotCount(): number {
Expand Down
18 changes: 17 additions & 1 deletion src/util/xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1021,8 +1021,9 @@ export const notations = createNamedElementFactory<
tuplets: NamedElement<'tuplet'>[];
arpeggiate: NamedElement<'arpeggiate'>;
ornaments: NamedElement<'ornaments'>[];
fermatas: NamedElement<'fermata'>[];
}
>('notations', (e, { tieds, slurs, tuplets, arpeggiate, ornaments }) => {
>('notations', (e, { tieds, slurs, tuplets, arpeggiate, ornaments, fermatas }) => {
if (tieds) {
e.append(...tieds);
}
Expand All @@ -1038,6 +1039,9 @@ export const notations = createNamedElementFactory<
if (ornaments) {
e.append(...ornaments);
}
if (fermatas) {
e.append(...fermatas);
}
});

export const arpeggiate = createNamedElementFactory<
Expand Down Expand Up @@ -1329,3 +1333,15 @@ export const rootfile = createNamedElementFactory<'rootfile', { fullPath: string
}
}
);

export const fermata = createNamedElementFactory<'fermata', { shape: string; type: string }>(
'fermata',
(e, { shape, type }) => {
if (shape) {
e.setTextContent(shape);
}
if (type) {
e.setAttribute('type', type);
}
}
);
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/integration/lilypond.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe('lilypond', () => {
{ filename: '24f-GraceNote-Slur.musicxml', width: 900 },
// { filename: '31a-Directions.musicxml', width: 900 },
{ filename: '31c-MetronomeMarks.musicxml', width: 900 },
// { filename: '32a-Notations.musicxml', width: 900 },
{ filename: '32a-Notations.musicxml', width: 900 },
// { filename: '32b-Articulations-Texts.musicxml', width: 900 },
// { filename: '32c-MultipleNotationChildren.musicxml', width: 900 },
// { filename: '32d-Arpeggio.musicxml', width: 900 },
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/vexml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('vexml', () => {
});

it.each<TestCase>([
// { filename: 'multi_system_spanners.musicxml', width: 400 },
{ filename: 'multi_system_spanners.musicxml', width: 400 },
{ filename: 'multi_stave_single_part_formatting.musicxml', width: 900 },
{ filename: 'multi_part_formatting.musicxml', width: 900 },
{ filename: 'complex_formatting.musicxml', width: 900 },
Expand Down
44 changes: 44 additions & 0 deletions tests/unit/musicxml/fermata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { FERMATA_SHAPES, FERMATA_TYPES, Fermata } from '@/musicxml';
import { xml } from '@/util';

describe(Fermata, () => {
describe('getShape', () => {
it.each(FERMATA_SHAPES.values)('returns the shape of the fermata', (shape) => {
const node = xml.fermata({ shape });
const fermata = new Fermata(node);
expect(fermata.getShape()).toBe(shape);
});

it('defaults to normal when missing', () => {
const node = xml.fermata();
const fermata = new Fermata(node);
expect(fermata.getShape()).toBe('normal');
});

it('defaults to normal when invalid', () => {
const node = xml.fermata({ shape: 'foo' });
const fermata = new Fermata(node);
expect(fermata.getShape()).toBe('normal');
});
});

describe('getType', () => {
it.each(FERMATA_TYPES.values)('returns the type of the fermata', (type) => {
const node = xml.fermata({ type });
const fermata = new Fermata(node);
expect(fermata.getType()).toBe(type);
});

it('defaults to upright when missing', () => {
const node = xml.fermata();
const fermata = new Fermata(node);
expect(fermata.getType()).toBe('upright');
});

it('defaults to upright when invalid', () => {
const node = xml.fermata({ type: 'foo' });
const fermata = new Fermata(node);
expect(fermata.getType()).toBe('upright');
});
});
});
17 changes: 16 additions & 1 deletion tests/unit/musicxml/notations.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { xml } from '@/util';
import { Tied, Notations, Ornaments, Slur, Tuplet, VERTICAL_DIRECTIONS } from '@/musicxml';
import { Tied, Notations, Ornaments, Slur, Tuplet, VERTICAL_DIRECTIONS, Fermata } from '@/musicxml';

describe(Notations, () => {
describe('isArpeggiated', () => {
Expand Down Expand Up @@ -119,4 +119,19 @@ describe(Notations, () => {
expect(notations.getOrnaments()).toStrictEqual([]);
});
});

describe('getFermatas', () => {
it('returns the fermatas of the notations', () => {
const fermata = xml.fermata();
const node = xml.notations({ fermatas: [fermata] });
const notations = new Notations(node);
expect(notations.getFermatas()).toStrictEqual([new Fermata(fermata)]);
});

it('defaults to an empty array', () => {
const node = xml.notations();
const notations = new Notations(node);
expect(notations.getFermatas()).toStrictEqual([]);
});
});
});

0 comments on commit b01dcc3

Please sign in to comment.