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 000000000..11dbf139f Binary files /dev/null and b/test-data/visual-tests/effects-and-annotations/ornaments.gp differ 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 000000000..9a29da46c Binary files /dev/null and b/test-data/visual-tests/effects-and-annotations/ornaments.png differ diff --git a/test/importer/AlphaTexImporter.test.ts b/test/importer/AlphaTexImporter.test.ts index 667fd82d5..07569329c 100644 --- a/test/importer/AlphaTexImporter.test.ts +++ b/test/importer/AlphaTexImporter.test.ts @@ -24,6 +24,9 @@ import { Settings } from '@src/Settings'; import { assert, expect } from 'chai'; import { ModelUtils } from '@src/model/ModelUtils'; import { GolpeType } from '@src/model/GolpeType'; +import { FadeType } from '@src/model/FadeType'; +import { BarreShape } from '@src/model/BarreShape'; +import { NoteOrnament } from '@src/model/NoteOrnament'; describe('AlphaTexImporterTest', () => { 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'); + }); });