From a6d524627c2b59c77b00ba6e3ed9e310900df6e7 Mon Sep 17 00:00:00 2001 From: Daniel Kuschny Date: Sun, 10 Nov 2024 22:22:11 +0100 Subject: [PATCH] feat: Add note ornaments (#1747) --- src/Environment.ts | 2 + src/NotationSettings.ts | 7 +++- src/exporter/GpifWriter.ts | 5 +++ src/generated/model/NoteCloner.ts | 1 + src/generated/model/NoteSerializer.ts | 5 +++ src/importer/AlphaTexImporter.ts | 16 ++++++- src/importer/GpifParser.ts | 17 ++++++++ src/model/MusicFontSymbol.ts | 4 ++ src/model/Note.ts | 6 +++ src/model/NoteOrnament.ts | 10 +++++ .../effects/NoteOrnamentEffectInfo.ts | 39 ++++++++++++++++++ src/rendering/glyphs/NoteOrnamentGlyph.ts | 34 +++++++++++++++ .../effects-and-annotations/ornaments.gp | Bin 0 -> 12362 bytes .../effects-and-annotations/ornaments.png | Bin 0 -> 10078 bytes test/importer/AlphaTexImporter.test.ts | 27 ++++++++++++ .../features/EffectsAndAnnotations.test.ts | 4 ++ 16 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 src/model/NoteOrnament.ts create mode 100644 src/rendering/effects/NoteOrnamentEffectInfo.ts create mode 100644 src/rendering/glyphs/NoteOrnamentGlyph.ts create mode 100644 test-data/visual-tests/effects-and-annotations/ornaments.gp create mode 100644 test-data/visual-tests/effects-and-annotations/ornaments.png diff --git a/src/Environment.ts b/src/Environment.ts index e5ecff464..6b26aef23 100644 --- a/src/Environment.ts +++ b/src/Environment.ts @@ -69,6 +69,7 @@ import { GolpeEffectInfo } from './rendering/effects/GolpeEffectInfo'; import { GolpeType } from './model/GolpeType'; import { WahPedalEffectInfo } from './rendering/effects/WahPedalEffectInfo'; import { BeatBarreEffectInfo } from './rendering/effects/BeatBarreEffectInfo'; +import { NoteOrnamentEffectInfo } from './rendering/effects/NoteOrnamentEffectInfo'; export class LayoutEngineFactory { public readonly vertical: boolean; @@ -511,6 +512,7 @@ export class Environment { new EffectBarRendererFactory(Environment.StaffIdBeforeScoreAlways, [ new FermataEffectInfo(), new BeatBarreEffectInfo(), + new NoteOrnamentEffectInfo(), new WahPedalEffectInfo(), ]), new EffectBarRendererFactory( diff --git a/src/NotationSettings.ts b/src/NotationSettings.ts index 9386672ae..6f16421a7 100644 --- a/src/NotationSettings.ts +++ b/src/NotationSettings.ts @@ -316,7 +316,12 @@ export enum NotationElement { /** * The Beat barre effect signs above and below the staff "1/2B IV ─────┐" */ - BeatBarre + EffectBeatBarre, + + /** + * The note ornaments like turns and mordents. + */ + EffectNoteOrnament, } /** diff --git a/src/exporter/GpifWriter.ts b/src/exporter/GpifWriter.ts index 2cee50d8d..f38ab649b 100644 --- a/src/exporter/GpifWriter.ts +++ b/src/exporter/GpifWriter.ts @@ -23,6 +23,7 @@ import { MasterBar } from '@src/model/MasterBar'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; import { Note } from '@src/model/Note'; import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; +import { NoteOrnament } from '@src/model/NoteOrnament'; import { Ottavia } from '@src/model/Ottavia'; import { PercussionMapper } from '@src/model/PercussionMapper'; import { PickStroke } from '@src/model/PickStroke'; @@ -382,6 +383,10 @@ export class GpifWriter { } else { noteNode.addElement('InstrumentArticulation').innerText = '0'; } + + if(note.ornament !== NoteOrnament.None){ + noteNode.addElement('Ornament').innerText = NoteOrnament[note.ornament]; + } } private writeNoteProperties(parent: XmlNode, note: Note) { diff --git a/src/generated/model/NoteCloner.ts b/src/generated/model/NoteCloner.ts index b6a035216..5b0311a72 100644 --- a/src/generated/model/NoteCloner.ts +++ b/src/generated/model/NoteCloner.ts @@ -48,6 +48,7 @@ export class NoteCloner { clone.durationPercent = original.durationPercent; clone.accidentalMode = original.accidentalMode; clone.dynamics = original.dynamics; + clone.ornament = original.ornament; return clone; } } diff --git a/src/generated/model/NoteSerializer.ts b/src/generated/model/NoteSerializer.ts index df2fa0724..846a30742 100644 --- a/src/generated/model/NoteSerializer.ts +++ b/src/generated/model/NoteSerializer.ts @@ -18,6 +18,7 @@ import { Fingers } from "@src/model/Fingers"; import { Duration } from "@src/model/Duration"; import { NoteAccidentalMode } from "@src/model/NoteAccidentalMode"; import { DynamicValue } from "@src/model/DynamicValue"; +import { NoteOrnament } from "@src/model/NoteOrnament"; export class NoteSerializer { public static fromJson(obj: Note, m: unknown): void { if (!m) { @@ -67,6 +68,7 @@ export class NoteSerializer { o.set("durationpercent", obj.durationPercent); o.set("accidentalmode", obj.accidentalMode as number); o.set("dynamics", obj.dynamics as number); + o.set("ornament", obj.ornament as number); obj.toJson(o); return o; } @@ -184,6 +186,9 @@ export class NoteSerializer { case "dynamics": obj.dynamics = JsonHelper.parseEnum(v, DynamicValue)!; return true; + case "ornament": + obj.ornament = JsonHelper.parseEnum(v, NoteOrnament)!; + return true; } return obj.setProperty(property, v); } diff --git a/src/importer/AlphaTexImporter.ts b/src/importer/AlphaTexImporter.ts index 7074e42d1..34c62d423 100644 --- a/src/importer/AlphaTexImporter.ts +++ b/src/importer/AlphaTexImporter.ts @@ -43,6 +43,7 @@ import { GolpeType } from '@src/model/GolpeType'; import { FadeType } from '@src/model/FadeType'; import { WahPedal } from '@src/model/WahPedal'; import { BarreShape } from '@src/model/BarreShape'; +import { NoteOrnament } from '@src/model/NoteOrnament'; /** * A list of terminals recognized by the alphaTex-parser @@ -1648,6 +1649,7 @@ export class AlphaTexImporter extends ScoreImporter { this.error('beat-barre', AlphaTexSymbols.Number, true); } beat.barreFret = this._syData as number; + beat.barreShape = BarreShape.Full; this._sy = this.newSy(); if (this._sy === AlphaTexSymbols.String) { @@ -1662,7 +1664,7 @@ export class AlphaTexImporter extends ScoreImporter { break; } } - + return true; } else { // string didn't match any beat effect syntax @@ -2007,6 +2009,18 @@ export class AlphaTexImporter extends ScoreImporter { note.accidentalMode = ModelUtils.parseAccidentalMode(this._syData as string); this._sy = this.newSy(); + } else if (syData === 'turn') { + this._sy = this.newSy(); + note.ornament = NoteOrnament.Turn; + } else if (syData === 'iturn') { + this._sy = this.newSy(); + note.ornament = NoteOrnament.InvertedTurn; + } else if (syData === 'umordent') { + this._sy = this.newSy(); + note.ornament = NoteOrnament.UpperMordent; + } else if (syData === 'lmordent') { + this._sy = this.newSy(); + note.ornament = NoteOrnament.LowerMordent; } else if (this.applyBeatEffect(note.beat)) { // Success } else { diff --git a/src/importer/GpifParser.ts b/src/importer/GpifParser.ts index 90040cd01..594b7c847 100644 --- a/src/importer/GpifParser.ts +++ b/src/importer/GpifParser.ts @@ -50,6 +50,7 @@ import { GolpeType } from '@src/model/GolpeType'; import { FadeType } from '@src/model/FadeType'; import { WahPedal } from '@src/model/WahPedal'; import { BarreShape } from '@src/model/BarreShape'; +import { NoteOrnament } from '@src/model/NoteOrnament'; /** * This structure represents a duration within a gpif @@ -1975,6 +1976,22 @@ export class GpifParser { case 'InstrumentArticulation': note.percussionArticulation = parseInt(c.innerText); break; + case 'Ornament': + switch (c.innerText) { + case 'Turn': + note.ornament = NoteOrnament.Turn; + break; + case 'InvertedTurn': + note.ornament = NoteOrnament.InvertedTurn; + break; + case 'UpperMordent': + note.ornament = NoteOrnament.UpperMordent; + break; + case 'LowerMordent': + note.ornament = NoteOrnament.LowerMordent; + break; + } + break; } } } diff --git a/src/model/MusicFontSymbol.ts b/src/model/MusicFontSymbol.ts index e95c367e4..a5e5ebf2d 100644 --- a/src/model/MusicFontSymbol.ts +++ b/src/model/MusicFontSymbol.ts @@ -130,6 +130,10 @@ export enum MusicFontSymbol { DynamicFFF = 0xe530, OrnamentTrill = 0xe566, + OrnamentTurn = 0xe567, + OrnamentTurnInverted = 0xe568, + OrnamentShortTrill = 0xe56c, + OrnamentMordent = 0xe56d, StringsDownBow = 0xe610, StringsUpBow = 0xe612, diff --git a/src/model/Note.ts b/src/model/Note.ts index 18d311d00..f674b0d7c 100644 --- a/src/model/Note.ts +++ b/src/model/Note.ts @@ -20,6 +20,7 @@ import { Logger } from '@src/Logger'; import { ModelUtils } from '@src/model/ModelUtils'; import { PickStroke } from '@src/model/PickStroke'; import { PercussionMapper } from '@src/model/PercussionMapper'; +import { NoteOrnament } from './NoteOrnament'; class NoteIdBag { public tieDestinationNoteId: number = -1; @@ -498,6 +499,11 @@ export class Note { */ public effectSlurDestination: Note | null = null; + /** + * The ornament applied on the note. + */ + public ornament: NoteOrnament = NoteOrnament.None; + public get stringTuning(): number { return this.beat.voice.bar.staff.capo + Note.getStringTuning(this.beat.voice.bar.staff, this.string); } diff --git a/src/model/NoteOrnament.ts b/src/model/NoteOrnament.ts new file mode 100644 index 000000000..456b3c1ff --- /dev/null +++ b/src/model/NoteOrnament.ts @@ -0,0 +1,10 @@ +/** + * Lists all note ornaments. + */ +export enum NoteOrnament { + None, + InvertedTurn, + Turn, + UpperMordent, + LowerMordent +} diff --git a/src/rendering/effects/NoteOrnamentEffectInfo.ts b/src/rendering/effects/NoteOrnamentEffectInfo.ts new file mode 100644 index 000000000..5afcb69a4 --- /dev/null +++ b/src/rendering/effects/NoteOrnamentEffectInfo.ts @@ -0,0 +1,39 @@ +import { Beat } from '@src/model'; +import { NotationElement } from '@src/NotationSettings'; +import { BarRendererBase } from '../BarRendererBase'; +import { EffectBarGlyphSizing } from '../EffectBarGlyphSizing'; +import { EffectBarRendererInfo } from '../EffectBarRendererInfo'; +import { EffectGlyph } from '../glyphs/EffectGlyph'; +import { Settings } from '@src/Settings'; +import { NoteOrnament } from '@src/model/NoteOrnament'; +import { NoteOrnamentGlyph } from '../glyphs/NoteOrnamentGlyph'; + +export class NoteOrnamentEffectInfo extends EffectBarRendererInfo { + public get notationElement(): NotationElement { + return NotationElement.EffectNoteOrnament; + } + + public get hideOnMultiTrack(): boolean { + return false; + } + + public get canShareBand(): boolean { + return true; + } + + public get sizingMode(): EffectBarGlyphSizing { + return EffectBarGlyphSizing.SingleOnBeat; + } + + public shouldCreateGlyph(settings: Settings, beat: Beat): boolean { + return beat.notes.some(n => n.ornament !== NoteOrnament.None); + } + + public createNewGlyph(renderer: BarRendererBase, beat: Beat): EffectGlyph { + return new NoteOrnamentGlyph(beat.notes.find(n=>n.ornament != NoteOrnament.None)!.ornament); + } + + public canExpand(from: Beat, to: Beat): boolean { + return false; + } +} diff --git a/src/rendering/glyphs/NoteOrnamentGlyph.ts b/src/rendering/glyphs/NoteOrnamentGlyph.ts new file mode 100644 index 000000000..b0e8bc342 --- /dev/null +++ b/src/rendering/glyphs/NoteOrnamentGlyph.ts @@ -0,0 +1,34 @@ +import { NoteOrnament } from '@src/model/NoteOrnament'; +import { MusicFontGlyph } from './MusicFontGlyph'; +import { MusicFontSymbol } from '@src/model'; +import { ICanvas } from '@src/platform'; + +export class NoteOrnamentGlyph extends MusicFontGlyph { + constructor(ornament: NoteOrnament) { + super(0, 0, 1, NoteOrnamentGlyph.getSymbol(ornament)); + this.center = true; + } + + public override doLayout(): void { + this.width = 26 * this.scale; + this.height = 18 * this.scale; + } + + private static getSymbol(ornament: NoteOrnament): MusicFontSymbol { + switch (ornament) { + case NoteOrnament.InvertedTurn: + return MusicFontSymbol.OrnamentTurnInverted; + case NoteOrnament.Turn: + return MusicFontSymbol.OrnamentTurn; + case NoteOrnament.UpperMordent: + return MusicFontSymbol.OrnamentShortTrill; + case NoteOrnament.LowerMordent: + return MusicFontSymbol.OrnamentMordent; + } + return MusicFontSymbol.None; + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + super.paint(cx, cy + this.height - 4 * this.scale, canvas); + } +} diff --git a/test-data/visual-tests/effects-and-annotations/ornaments.gp b/test-data/visual-tests/effects-and-annotations/ornaments.gp new file mode 100644 index 0000000000000000000000000000000000000000..11dbf139ffb89412d1f7151f98612310a40339dd GIT binary patch literal 12362 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 literal 0 HcmV?d00001 diff --git a/test-data/visual-tests/effects-and-annotations/ornaments.png b/test-data/visual-tests/effects-and-annotations/ornaments.png new file mode 100644 index 0000000000000000000000000000000000000000..9a29da46c08aa18a9f74d3cab98f7080855e7e8b GIT binary patch literal 10078 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 L { function parseTex(tex: string): Score { @@ -1312,4 +1315,28 @@ describe('AlphaTexImporterTest', () => { expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].golpe).to.equal(GolpeType.Finger); expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].golpe).to.equal(GolpeType.Thumb); }); + + it('fade', () => { + let score = parseTex('3.3 { f } 3.3 { fo } 3.3 { vs } '); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].fade).to.equal(FadeType.FadeIn); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].fade).to.equal(FadeType.FadeOut); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].fade).to.equal(FadeType.VolumeSwell); + }); + + it('barre', () => { + let score = parseTex('3.3 { barre 5 } 3.3 { barre 14 half }'); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].barreFret).to.equal(5); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].barreShape).to.equal(BarreShape.Full); + + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].barreFret).to.equal(14); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].barreShape).to.equal(BarreShape.Half); + }); + + it('ornaments', () => { + let score = parseTex('3.3 { turn } 3.3 { iturn } 3.3 { umordent } 3.3 { lmordent }'); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[0].notes[0].ornament).to.equal(NoteOrnament.Turn); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[1].notes[0].ornament).to.equal(NoteOrnament.InvertedTurn); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[2].notes[0].ornament).to.equal(NoteOrnament.UpperMordent); + expect(score.tracks[0].staves[0].bars[0].voices[0].beats[3].notes[0].ornament).to.equal(NoteOrnament.LowerMordent); + }); }); diff --git a/test/visualTests/features/EffectsAndAnnotations.test.ts b/test/visualTests/features/EffectsAndAnnotations.test.ts index c5f0bf830..d373d72bf 100644 --- a/test/visualTests/features/EffectsAndAnnotations.test.ts +++ b/test/visualTests/features/EffectsAndAnnotations.test.ts @@ -230,4 +230,8 @@ describe('EffectsAndAnnotationsTests', () => { it('barre', async () => { await VisualTestHelper.runVisualTest('effects-and-annotations/barre.gp'); }); + + it('ornaments', async () => { + await VisualTestHelper.runVisualTest('effects-and-annotations/ornaments.gp'); + }); });