diff --git a/src/Environment.ts b/src/Environment.ts index dda8864b2..27ae63131 100644 --- a/src/Environment.ts +++ b/src/Environment.ts @@ -64,6 +64,7 @@ import { NumberedBarRendererFactory } from './rendering/NumberedBarRendererFacto import { FreeTimeEffectInfo } from './rendering/effects/FreeTimeEffectInfo'; import { ScoreBarRenderer } from './rendering/ScoreBarRenderer'; import { TabBarRenderer } from './rendering/TabBarRenderer'; +import { SustainPedalEffectInfo } from './rendering/effects/SustainPedalEffectInfo'; export class LayoutEngineFactory { public readonly vertical: boolean; @@ -525,7 +526,8 @@ export class Environment { new EffectBarRendererFactory(Environment.StaffIdBeforeNumberedAlways, [ new CrescendoEffectInfo(), new OttaviaEffectInfo(false), - new DynamicsEffectInfo() + new DynamicsEffectInfo(), + new SustainPedalEffectInfo() ]), // no before-numbered-hideable new NumberedBarRendererFactory(), diff --git a/src/NotationSettings.ts b/src/NotationSettings.ts index 0099886c1..461816c59 100644 --- a/src/NotationSettings.ts +++ b/src/NotationSettings.ts @@ -292,7 +292,12 @@ export enum NotationElement { /** * The "Free time" text shown above the staff. */ - EffectFreeTime + EffectFreeTime, + + /** + * The Sustain pedal effect shown above the staff "Ped.____*" + */ + EffectSustainPedal } /** diff --git a/src/exporter/GpifWriter.ts b/src/exporter/GpifWriter.ts index 8ce079d4b..0431d85aa 100644 --- a/src/exporter/GpifWriter.ts +++ b/src/exporter/GpifWriter.ts @@ -2,7 +2,7 @@ import { GeneralMidi } from '@src/midi/GeneralMidi'; import { MidiUtils } from '@src/midi/MidiUtils'; import { AccentuationType } from '@src/model/AccentuationType'; import { AutomationType } from '@src/model/Automation'; -import { Bar } from '@src/model/Bar'; +import { Bar, SustainPedalMarkerType } from '@src/model/Bar'; import { Beat } from '@src/model/Beat'; import { BendPoint } from '@src/model/BendPoint'; import { BrushType } from '@src/model/BrushType'; @@ -390,7 +390,7 @@ export class GpifWriter { this.writeSimplePropertyNode(properties, 'String', 'String', (note.string - 1).toString()); this.writeSimplePropertyNode(properties, 'Fret', 'Fret', note.fret.toString()); this.writeSimplePropertyNode(properties, 'Midi', 'Number', note.realValue.toString()); - if(note.showStringNumber) { + if (note.showStringNumber) { this.writeSimplePropertyNode(properties, 'ShowStringNumber', 'Enable', null); } } @@ -737,7 +737,7 @@ export class GpifWriter { } beatNode.addElement('ConcertPitchStemOrientation').innerText = 'Undefined'; - if(beat.slashed) { + if (beat.slashed) { beatNode.addElement('Slashed'); } if (!beat.isRest) { @@ -953,7 +953,7 @@ export class GpifWriter { masterTrackNode.addElement('Anacrusis'); } - if(score.masterBars[0].tempoAutomations.length === 0){ + if (score.masterBars[0].tempoAutomations.length === 0) { const initialTempoAutomation = automations.addElement('Automation'); initialTempoAutomation.addElement('Type').innerText = 'Tempo'; initialTempoAutomation.addElement('Linear').innerText = 'false'; @@ -965,7 +965,7 @@ export class GpifWriter { initialTempoAutomation.addElement('Text').innerText = score.tempoLabel; } } - + for (const mb of score.masterBars) { for (const automation of mb.tempoAutomations) { const tempoAutomation = automations.addElement('Automation'); @@ -1135,6 +1135,29 @@ export class GpifWriter { } } } + + for (const s of track.staves) { + for (const b of s.bars) { + for (const sustainPedal of b.sustainPedals) { + if (sustainPedal.pedalType !== SustainPedalMarkerType.Hold) { + const automation = automationsNode.addElement('Automation'); + automation.addElement('Type').innerText = 'SustainPedal'; + automation.addElement('Linear').innerText = 'false'; + automation.addElement('Bar').innerText = b.index.toString(); + automation.addElement('Position').innerText = sustainPedal.ratioPosition.toString(); + automation.addElement('Visible').innerText = 'true'; + switch (sustainPedal.pedalType) { + case SustainPedalMarkerType.Down: + automation.addElement('Value').innerText = `0 1`; + break; + case SustainPedalMarkerType.Up: + automation.addElement('Value').innerText = `0 3`; + break; + } + } + } + } + } } private writeMidiConnectionNode(trackNode: XmlNode, track: Track) { @@ -1557,10 +1580,8 @@ export class GpifWriter { 'Time' ).innerText = `${masterBar.timeSignatureNumerator}/${masterBar.timeSignatureDenominator}`; - if(masterBar.isFreeTime) { - masterBarNode.addElement( - 'FreeTime' - ); + if (masterBar.isFreeTime) { + masterBarNode.addElement('FreeTime'); } let bars: string[] = []; diff --git a/src/generated/model/BarSerializer.ts b/src/generated/model/BarSerializer.ts index b9ea9a28a..0ea3bced7 100644 --- a/src/generated/model/BarSerializer.ts +++ b/src/generated/model/BarSerializer.ts @@ -6,10 +6,12 @@ import { Bar } from "@src/model/Bar"; import { JsonHelper } from "@src/io/JsonHelper"; import { VoiceSerializer } from "@src/generated/model/VoiceSerializer"; +import { SustainPedalMarkerSerializer } from "@src/generated/model/SustainPedalMarkerSerializer"; import { Clef } from "@src/model/Clef"; import { Ottavia } from "@src/model/Ottavia"; import { Voice } from "@src/model/Voice"; import { SimileMark } from "@src/model/SimileMark"; +import { SustainPedalMarker } from "@src/model/Bar"; export class BarSerializer { public static fromJson(obj: Bar, m: unknown): void { if (!m) { @@ -29,6 +31,7 @@ export class BarSerializer { o.set("similemark", obj.simileMark as number); o.set("displayscale", obj.displayScale); o.set("displaywidth", obj.displayWidth); + o.set("sustainpedals", obj.sustainPedals.map(i => SustainPedalMarkerSerializer.toJson(i))); return o; } public static setProperty(obj: Bar, property: string, v: unknown): boolean { @@ -59,6 +62,14 @@ export class BarSerializer { case "displaywidth": obj.displayWidth = v! as number; return true; + case "sustainpedals": + obj.sustainPedals = []; + for (const o of (v as (Map<string, unknown> | null)[])) { + const i = new SustainPedalMarker(); + SustainPedalMarkerSerializer.fromJson(i, o); + obj.sustainPedals.push(i); + } + return true; } return false; } diff --git a/src/generated/model/SustainPedalMarkerSerializer.ts b/src/generated/model/SustainPedalMarkerSerializer.ts new file mode 100644 index 000000000..6c771235b --- /dev/null +++ b/src/generated/model/SustainPedalMarkerSerializer.ts @@ -0,0 +1,36 @@ +// <auto-generated> +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +import { SustainPedalMarker } from "@src/model/Bar"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { SustainPedalMarkerType } from "@src/model/Bar"; +export class SustainPedalMarkerSerializer { + public static fromJson(obj: SustainPedalMarker, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k, v)); + } + public static toJson(obj: SustainPedalMarker | null): Map<string, unknown> | null { + if (!obj) { + return null; + } + const o = new Map<string, unknown>(); + o.set("ratioposition", obj.ratioPosition); + o.set("pedaltype", obj.pedalType as number); + return o; + } + public static setProperty(obj: SustainPedalMarker, property: string, v: unknown): boolean { + switch (property) { + case "ratioposition": + obj.ratioPosition = v! as number; + return true; + case "pedaltype": + obj.pedalType = JsonHelper.parseEnum<SustainPedalMarkerType>(v, SustainPedalMarkerType)!; + return true; + } + return false; + } +} diff --git a/src/importer/AlphaTexImporter.ts b/src/importer/AlphaTexImporter.ts index 385e42ffe..c2c1c0679 100644 --- a/src/importer/AlphaTexImporter.ts +++ b/src/importer/AlphaTexImporter.ts @@ -3,7 +3,7 @@ import { ScoreImporter } from '@src/importer/ScoreImporter'; import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; import { AccentuationType } from '@src/model/AccentuationType'; import { Automation, AutomationType } from '@src/model/Automation'; -import { Bar } from '@src/model/Bar'; +import { Bar, SustainPedalMarker, SustainPedalMarkerType } from '@src/model/Bar'; import { Beat } from '@src/model/Beat'; import { BendPoint } from '@src/model/BendPoint'; import { BrushType } from '@src/model/BrushType'; @@ -153,6 +153,7 @@ export class AlphaTexImporter extends ScoreImporter { private _staffHasExplicitTuning: boolean = false; private _staffTuningApplied: boolean = false; private _percussionArticulationNames = new Map<string, number>(); + private _sustainPedalToBeat = new Map<SustainPedalMarker, Beat>(); private _accidentalMode: AlphaTexAccidentalMode = AlphaTexAccidentalMode.Explicit; @@ -179,6 +180,8 @@ export class AlphaTexImporter extends ScoreImporter { } this._allowTuning = true; this._lyrics = new Map<number, Lyrics[]>(); + this._sustainPedalToBeat = new Map<SustainPedalMarker, Beat>(); + this.createDefaultScore(); this._curChPos = 0; this._line = 1; @@ -206,6 +209,12 @@ export class AlphaTexImporter extends ScoreImporter { for (const [track, lyrics] of this._lyrics) { this._score.tracks[track].applyLyrics(lyrics); } + for (const [sustainPedal, beat] of this._sustainPedalToBeat) { + if(sustainPedal.ratioPosition === 0) { + const duration = beat.voice.bar.masterBar.calculateDuration(); + sustainPedal.ratioPosition = beat.playbackStart / duration; + } + } return this._score; } catch (e) { if (e instanceof AlphaTexError) { @@ -1576,6 +1585,30 @@ export class AlphaTexImporter extends ScoreImporter { this._sy = this.newSy(); } return true; + } else if (syData === 'spd') { + const sustainPedal = new SustainPedalMarker(); + sustainPedal.pedalType = SustainPedalMarkerType.Down; + // exact ratio position will be applied after .finish() when times are known + this._sustainPedalToBeat.set(sustainPedal, beat); + beat.voice.bar.sustainPedals.push(sustainPedal); + this._sy = this.newSy(); + return true; + } else if (syData === 'spu') { + const sustainPedal = new SustainPedalMarker(); + sustainPedal.pedalType = SustainPedalMarkerType.Up; + // exact ratio position will be applied after .finish() when times are known + this._sustainPedalToBeat.set(sustainPedal, beat); + beat.voice.bar.sustainPedals.push(sustainPedal); + this._sy = this.newSy(); + return true; + } else if (syData === 'spe') { + const sustainPedal = new SustainPedalMarker(); + sustainPedal.pedalType = SustainPedalMarkerType.Up; + sustainPedal.ratioPosition = 1; + this._sustainPedalToBeat.set(sustainPedal, beat); + beat.voice.bar.sustainPedals.push(sustainPedal); + this._sy = this.newSy(); + return true; } else if (syData === 'slashed') { beat.slashed = true; this._sy = this.newSy(); diff --git a/src/importer/GpifParser.ts b/src/importer/GpifParser.ts index 9ca9c376e..8cb4305eb 100644 --- a/src/importer/GpifParser.ts +++ b/src/importer/GpifParser.ts @@ -1,7 +1,7 @@ import { UnsupportedFormatError } from '@src/importer/UnsupportedFormatError'; import { AccentuationType } from '@src/model/AccentuationType'; import { Automation, AutomationType } from '@src/model/Automation'; -import { Bar } from '@src/model/Bar'; +import { Bar, SustainPedalMarker, SustainPedalMarkerType } from '@src/model/Bar'; import { Beat } from '@src/model/Beat'; import { BendPoint } from '@src/model/BendPoint'; import { BrushType } from '@src/model/BrushType'; @@ -91,6 +91,7 @@ export class GpifParser { private _masterTrackAutomations!: Map<number, Automation[]>; private _automationsPerTrackIdAndBarIndex!: Map<string, Map<number, Automation[]>>; + private _sustainPedalsPerTrackIdAndBarIndex!: Map<string, Map<number, SustainPedalMarker[]>>; private _tracksMapping!: string[]; private _tracksById!: Map<string, Track>; private _masterBars!: MasterBar[]; @@ -114,6 +115,7 @@ export class GpifParser { public parseXml(xml: string, settings: Settings): void { this._masterTrackAutomations = new Map<number, Automation[]>(); this._automationsPerTrackIdAndBarIndex = new Map<string, Map<number, Automation[]>>(); + this._sustainPedalsPerTrackIdAndBarIndex = new Map<string, Map<number, SustainPedalMarker[]>>(); this._tracksMapping = []; this._tracksById = new Map<string, Track>(); this._masterBars = []; @@ -254,7 +256,6 @@ export class GpifParser { case 'ScoreSystemsLayout': this.score.systemsLayout = c.innerText.split(' ').map(i => parseInt(i)); break; - } } } @@ -268,7 +269,7 @@ export class GpifParser { if (c.nodeType === XmlNodeType.Element) { switch (c.localName) { case 'Automations': - this.parseAutomations(c, this._masterTrackAutomations, null); + this.parseAutomations(c, this._masterTrackAutomations, null, null); break; case 'Tracks': this._tracksMapping = c.innerText.split(' '); @@ -281,19 +282,29 @@ export class GpifParser { } } - private parseAutomations(node: XmlNode, automations: Map<number, Automation[]>, sounds: Map<string, GpifSound> | null): void { + private parseAutomations( + node: XmlNode, + automations: Map<number, Automation[]>, + sounds: Map<string, GpifSound> | null, + sustainPedals: Map<number, SustainPedalMarker[]> | null + ): void { for (let c of node.childNodes) { if (c.nodeType === XmlNodeType.Element) { switch (c.localName) { case 'Automation': - this.parseAutomation(c, automations, sounds); + this.parseAutomation(c, automations, sounds, sustainPedals); break; } } } } - private parseAutomation(node: XmlNode, automations: Map<number, Automation[]>, sounds: Map<string, GpifSound> | null): void { + private parseAutomation( + node: XmlNode, + automations: Map<number, Automation[]>, + sounds: Map<string, GpifSound> | null, + sustainPedals: Map<number, SustainPedalMarker[]> | null + ): void { let type: string | null = null; let isLinear: boolean = false; let barIndex: number = -1; @@ -349,8 +360,38 @@ export class GpifParser { break; case 'Sound': if (textValue && sounds && sounds.has(textValue)) { - automation = Automation.buildInstrumentAutomation(isLinear, ratioPosition, sounds.get(textValue)!.program); + automation = Automation.buildInstrumentAutomation( + isLinear, + ratioPosition, + sounds.get(textValue)!.program + ); } + break; + case 'SustainPedal': + // we expect sustain pedals only on track automations + if (sustainPedals) { + let v: SustainPedalMarker[]; + if (sustainPedals.has(barIndex)) { + v = sustainPedals.get(barIndex)!; + } else { + v = []; + sustainPedals.set(barIndex, v); + } + + const sustain = new SustainPedalMarker(); + sustain.ratioPosition = ratioPosition; + switch (reference) { + case 1: + sustain.pedalType = SustainPedalMarkerType.Down; + break; + case 3: + sustain.pedalType = SustainPedalMarkerType.Up; + break; + } + + v.push(sustain); + } + break; } if (automation) { @@ -469,9 +510,13 @@ export class GpifParser { } private parseTrackAutomations(trackId: string, c: XmlNode) { - const trackAutomations = new Map<number, Automation[]>() - this._automationsPerTrackIdAndBarIndex.set(trackId, trackAutomations) - this.parseAutomations(c, trackAutomations, this._soundsByTrack.get(trackId)!); + const trackAutomations = new Map<number, Automation[]>(); + this._automationsPerTrackIdAndBarIndex.set(trackId, trackAutomations); + + const sustainPedals = new Map<number, SustainPedalMarker[]>(); + this._sustainPedalsPerTrackIdAndBarIndex.set(trackId, sustainPedals); + + this.parseAutomations(c, trackAutomations, this._soundsByTrack.get(trackId)!, sustainPedals); } private parseNotationPatch(track: Track, node: XmlNode) { @@ -537,7 +582,7 @@ export class GpifParser { private parseElement(track: Track, node: XmlNode) { const typeElement = node.findChildElement('Type'); - const type = typeElement ? typeElement.innerText : ""; + const type = typeElement ? typeElement.innerText : ''; for (let c of node.childNodes) { if (c.nodeType === XmlNodeType.Element) { switch (c.localName) { @@ -861,9 +906,9 @@ export class GpifParser { private parseDiagramItemForChord(chord: Chord, node: XmlNode): void { chord.name = node.getAttribute('name'); - + let diagram = node.findChildElement('Diagram'); - if(!diagram) { + if (!diagram) { chord.showDiagram = false; chord.showFingering = false; return; @@ -1231,10 +1276,9 @@ export class GpifParser { case 'Fermatas': this.parseFermatas(masterBar, c); break; - case "XProperties": + case 'XProperties': this.parseMasterBarXProperties(c, masterBar); break; - } } } @@ -1361,7 +1405,7 @@ export class GpifParser { break; } break; - case "XProperties": + case 'XProperties': this.parseBarXProperties(c, bar); break; } @@ -1613,7 +1657,6 @@ export class GpifParser { } } - private parseBarXProperties(node: XmlNode, bar: Bar) { for (let c of node.childNodes) { if (c.nodeType === XmlNodeType.Element) { @@ -2130,7 +2173,7 @@ export class GpifParser { } private toBendOffset(gpxOffset: number): number { - return (gpxOffset * GpifParser.BendPointPositionFactor); + return gpxOffset * GpifParser.BendPointPositionFactor; } private parseRhythms(node: XmlNode): void { @@ -2299,7 +2342,7 @@ export class GpifParser { } } - // clear out percussion articulations where not needed + // clear out percussion articulations where not needed // and add automations for (let trackId of this._tracksMapping) { if (!trackId) { @@ -2320,18 +2363,32 @@ export class GpifParser { if (this._automationsPerTrackIdAndBarIndex.has(trackId)) { const trackAutomations = this._automationsPerTrackIdAndBarIndex.get(trackId)!; + for (const [barNumber, automations] of trackAutomations) { if (track.staves.length > 0 && barNumber < track.staves[0].bars.length) { const bar = track.staves[0].bars[barNumber]; if (bar.voices.length > 0 && bar.voices[0].beats.length > 0) { const beat = bar.voices[0].beats[0]; for (const a of automations) { + // NOTE: currently the automations of a bar are applied to the + // first beat of a bar beat.automations.push(a); } + } else { } } } } + + if (this._sustainPedalsPerTrackIdAndBarIndex.has(trackId)) { + const sustainPedals = this._sustainPedalsPerTrackIdAndBarIndex.get(trackId)!; + for (const [barNumber, markers] of sustainPedals) { + if (track.staves.length > 0 && barNumber < track.staves[0].bars.length) { + const bar = track.staves[0].bars[barNumber]; + bar.sustainPedals = markers; + } + } + } } // build masterbar automations diff --git a/src/model/Bar.ts b/src/model/Bar.ts index f9132904d..ee9de9b35 100644 --- a/src/model/Bar.ts +++ b/src/model/Bar.ts @@ -6,6 +6,60 @@ import { Staff } from '@src/model/Staff'; import { Voice } from '@src/model/Voice'; import { Settings } from '@src/Settings'; +/** + * The different pedal marker types. + */ +export enum SustainPedalMarkerType { + /** + * Indicates that the pedal should be pressed from this time on. + */ + Down, + /** + * Indicates that the pedal should be held on this marker (used when the pedal is held for the whole bar) + */ + Hold, + /** + * indicates that the pedal should be lifted up at this time. + */ + Up +} + +/** + * A marker on whether a sustain pedal starts or ends. + * @json + * @json_strict + */ +export class SustainPedalMarker { + /** + * The relative position of pedal markers within the bar. + */ + public ratioPosition: number = 0; + /** + * Whether what should be done with the pedal at this point + */ + public pedalType: SustainPedalMarkerType = SustainPedalMarkerType.Down; + + /** + * THe bar to which this marker belongs to. + * @json_ignore + */ + public bar!: Bar; + + /** + * The next pedal marker for linking the related markers together to a "down -> hold -> up" or "down -> up" sequence. + * Always null for "up" markers. + * @json_ignore + */ + public nextPedalMarker: SustainPedalMarker | null = null; + + /** + * The previous pedal marker for linking the related markers together to a "down -> hold -> up" or "down -> up" sequence. + * Always null for "down" markers. + * @json_ignore + */ + public previousPedalMarker: SustainPedalMarker | null = null; +} + /** * A bar is a single block within a track, also known as Measure. * @json @@ -71,16 +125,21 @@ export class Bar { public isMultiVoice: boolean = false; /** - * A relative scale for the size of the bar when displayed. The scale is relative + * A relative scale for the size of the bar when displayed. The scale is relative * within a single line (system). The sum of all scales in one line make the total width, * and then this individual scale gives the relative size. */ - public displayScale:number = 1; + public displayScale: number = 1; /** * An absolute width of the bar to use when displaying in single track display scenarios. */ - public displayWidth:number = -1; + public displayWidth: number = -1; + + /** + * The sustain pedal markers within this bar. + */ + public sustainPedals: SustainPedalMarker[] = []; public get masterBar(): MasterBar { return this.staff.track.score.masterBars[this.index]; @@ -106,10 +165,42 @@ export class Bar { for (let i: number = 0, j: number = this.voices.length; i < j; i++) { let voice: Voice = this.voices[i]; voice.finish(settings, sharedDataBag); - if(i > 0 && !voice.isEmpty) { + if (i > 0 && !voice.isEmpty) { this.isMultiVoice = true; } } + + // chain sustain pedal markers + if (this.sustainPedals.length > 0) { + let previousMarker: SustainPedalMarker | null = null; + + if (this.previousBar && this.previousBar.sustainPedals.length > 0) { + previousMarker = this.previousBar.sustainPedals[this.previousBar.sustainPedals.length - 1]; + } + + for (const marker of this.sustainPedals) { + if (previousMarker && previousMarker.pedalType !== SustainPedalMarkerType.Up) { + previousMarker.nextPedalMarker = marker; + marker.previousPedalMarker = previousMarker; + } + + marker.bar = this; + previousMarker = marker; + } + } else if (this.previousBar && this.previousBar.sustainPedals.length > 0) { + const lastMarker = this.previousBar.sustainPedals[this.previousBar.sustainPedals.length - 1]; + if (lastMarker.pedalType !== SustainPedalMarkerType.Up) { + // create hold marker if the last marker on the previous bar is not "up" + const holdMarker = new SustainPedalMarker(); + holdMarker.ratioPosition = 0; + holdMarker.bar = this; + holdMarker.pedalType = SustainPedalMarkerType.Hold; + this.sustainPedals.push(holdMarker); + + lastMarker.nextPedalMarker = holdMarker; + holdMarker.previousPedalMarker = lastMarker; + } + } } public calculateDuration(): number { diff --git a/src/model/Font.ts b/src/model/Font.ts index cccdaa14e..c712186c6 100644 --- a/src/model/Font.ts +++ b/src/model/Font.ts @@ -450,6 +450,10 @@ export class Font { this._css = this.toCssString(); } + public withSize(newSize:number) : Font { + return Font.withFamilyList(this._families, newSize, this._style, this._weight); + } + /** * Initializes a new instance of the {@link Font} class. * @param families The families. diff --git a/src/model/MusicFontSymbol.ts b/src/model/MusicFontSymbol.ts index 8f5ec6013..e79f9a5e0 100644 --- a/src/model/MusicFontSymbol.ts +++ b/src/model/MusicFontSymbol.ts @@ -134,6 +134,9 @@ export enum MusicFontSymbol { StringsDownBow = 0xe610, StringsUpBow = 0xe612, + KeyboardPedalPed = 0xE650, + KeyboardPedalUp = 0xE655, + PictEdgeOfCymbal = 0xe729, GuitarString0 = 0xe833, diff --git a/src/platform/skia/SkiaCanvas.ts b/src/platform/skia/SkiaCanvas.ts index 342c28746..1d44ecd18 100644 --- a/src/platform/skia/SkiaCanvas.ts +++ b/src/platform/skia/SkiaCanvas.ts @@ -256,7 +256,13 @@ export class SkiaCanvas implements ICanvas { */ private static readonly FontSizeToLineHeight = 1.2; + private _initialMeasure = true; public measureText(text: string) { + // BUG: for some reason the very initial measure text in alphaSkia delivers wrong results, so we it twice + if(this._initialMeasure) { + this._canvas.measureText(text, this.getTypeFace(), this.font.size * this.settings.display.scale); + this._initialMeasure = false; + } return new TextMetrics(this._canvas.measureText(text, this.getTypeFace(), this.font.size * this.settings.display.scale), this.font.size * SkiaCanvas.FontSizeToLineHeight); } diff --git a/src/rendering/EffectBarGlyphSizing.ts b/src/rendering/EffectBarGlyphSizing.ts index d6657109a..a08d38388 100644 --- a/src/rendering/EffectBarGlyphSizing.ts +++ b/src/rendering/EffectBarGlyphSizing.ts @@ -29,6 +29,7 @@ export enum EffectBarGlyphSizing { * the applied beat. */ GroupedOnBeatToEnd, + /** * The effect glyph is placed on the whole bar covering the whole width */ diff --git a/src/rendering/effects/SustainPedalEffectInfo.ts b/src/rendering/effects/SustainPedalEffectInfo.ts new file mode 100644 index 000000000..9a4824872 --- /dev/null +++ b/src/rendering/effects/SustainPedalEffectInfo.ts @@ -0,0 +1,38 @@ +import { NotationElement } from '@src/NotationSettings'; +import { EffectBarGlyphSizing } from '../EffectBarGlyphSizing'; +import { Settings } from '@src/Settings'; +import { Beat } from '@src/model'; +import { BarRendererBase } from '../BarRendererBase'; +import { EffectGlyph } from '../glyphs/EffectGlyph'; +import { EffectBarRendererInfo } from '../EffectBarRendererInfo'; +import { SustainPedalGlyph } from '../glyphs/SustainPedalGlyph'; + +export class SustainPedalEffectInfo extends EffectBarRendererInfo { + public get notationElement(): NotationElement { + return NotationElement.EffectSustainPedal; + } + + public get hideOnMultiTrack(): boolean { + return false; + } + + public get canShareBand(): boolean { + return false; + } + + public get sizingMode(): EffectBarGlyphSizing { + return EffectBarGlyphSizing.FullBar; + } + + public shouldCreateGlyph(settings: Settings, beat: Beat): boolean { + return beat.voice.index === 0 && beat.index === 0 && beat.voice.bar.sustainPedals.length > 0; + } + + public createNewGlyph(renderer: BarRendererBase, beat: Beat): EffectGlyph { + return new SustainPedalGlyph(); + } + + public canExpand(from: Beat, to: Beat): boolean { + return true; + } +} diff --git a/src/rendering/glyphs/SustainPedalGlyph.ts b/src/rendering/glyphs/SustainPedalGlyph.ts new file mode 100644 index 000000000..e1a41bef8 --- /dev/null +++ b/src/rendering/glyphs/SustainPedalGlyph.ts @@ -0,0 +1,102 @@ +import { ICanvas } from '@src/platform'; +import { EffectGlyph } from './EffectGlyph'; +import { SustainPedalMarker, SustainPedalMarkerType } from '@src/model/Bar'; +import { MusicFontSymbol } from '@src/model'; +import { BeatXPosition } from '../BeatXPosition'; + +export class SustainPedalGlyph extends EffectGlyph { + private static readonly TextHeight = 19; + private static readonly TextWidth = 35; + private static readonly TextLinePadding = 3; + + private static readonly StarSize = 16; + private static readonly StarLinePadding = 3; + + public constructor() { + super(0, 0); + } + + public override doLayout(): void { + super.doLayout(); + this.height = SustainPedalGlyph.TextHeight * this.scale; + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + const renderer = this.renderer; + + const firstOnNoteX = renderer.bar.isEmpty + ? renderer.beatGlyphsStart + : renderer.getBeatX(renderer.bar.voices[0].beats[0], BeatXPosition.OnNotes); + + const x = cx + this.x + firstOnNoteX; + const y = cy + this.y; + const w = renderer.postBeatGlyphsStart - firstOnNoteX; + const h = this.height; + + let markers = renderer.bar.sustainPedals; + + let markerIndex = 0; + while (markerIndex < markers.length) { + let marker: SustainPedalMarker | null = markers[markerIndex]; + while (marker != null) { + const markerX = x + w * marker.ratioPosition; + + // real own marker + let linePadding = 0; + if (marker.pedalType === SustainPedalMarkerType.Down) { + canvas.fillMusicFontSymbol(markerX, y + h, 1, MusicFontSymbol.KeyboardPedalPed, true); + linePadding = + (SustainPedalGlyph.TextWidth / 2) * this.scale + SustainPedalGlyph.TextLinePadding * this.scale; + } else if (marker.pedalType === SustainPedalMarkerType.Up) { + canvas.fillMusicFontSymbol(markerX, y + h, 1, MusicFontSymbol.KeyboardPedalUp, true); + linePadding = + (SustainPedalGlyph.StarSize / 2) * this.scale + SustainPedalGlyph.StarLinePadding * this.scale; + } + + // line to next marker or end-of-bar + if (marker.nextPedalMarker) { + if (marker.nextPedalMarker.bar === marker.bar) { + let nextX = x + w * marker.nextPedalMarker.ratioPosition; + + switch (marker.nextPedalMarker.pedalType) { + case SustainPedalMarkerType.Down: + nextX -= (SustainPedalGlyph.TextWidth / 2) * this.scale; + break; + case SustainPedalMarkerType.Hold: + // no offset on hold + break; + case SustainPedalMarkerType.Up: + nextX -= (SustainPedalGlyph.StarSize / 2) * this.scale; + break; + } + + const startX = markerX + linePadding; + if (nextX > startX) { + canvas.fillRect(startX, y + h - this.scale, nextX - startX, this.scale); + } + } else { + const nextX = cx + this.x + this.width; + const startX = markerX + linePadding; + canvas.fillRect(startX, y + h - this.scale, nextX - startX, this.scale); + } + } + + // line from bar start to initial marker + if (markerIndex === 0 && marker.previousPedalMarker) { + const startX = cx + this.x; + const endX = markerX - linePadding; + canvas.fillRect(startX, y + h - this.scale, endX - startX, this.scale); + } + + markerIndex++; + + if (marker.nextPedalMarker != null && marker.nextPedalMarker.bar !== marker.bar) { + marker = null; + markerIndex = markers.length; + } else { + marker = marker.nextPedalMarker; + } + } + } + } +} diff --git a/src/rendering/staves/BarLayoutingInfo.ts b/src/rendering/staves/BarLayoutingInfo.ts index 5d45e42cd..9a6eee088 100644 --- a/src/rendering/staves/BarLayoutingInfo.ts +++ b/src/rendering/staves/BarLayoutingInfo.ts @@ -300,8 +300,7 @@ export class BarLayoutingInfo { // cy -= height; // canvas.color = settings.display.resources.mainGlyphColor; - // const font = settings.display.resources.effectFont.clone(); - // font.size *= 0.8; + // const font = settings.display.resources.effectFont.withSize(settings.display.resources.effectFont.size * 0.8); // canvas.font = font; // canvas.fillText(force.toFixed(2), cx, cy); diff --git a/test-data/visual-tests/effects-and-annotations/sustain-1200.png b/test-data/visual-tests/effects-and-annotations/sustain-1200.png new file mode 100644 index 000000000..703553d76 Binary files /dev/null and b/test-data/visual-tests/effects-and-annotations/sustain-1200.png differ diff --git a/test-data/visual-tests/effects-and-annotations/sustain-600.png b/test-data/visual-tests/effects-and-annotations/sustain-600.png new file mode 100644 index 000000000..a49563f1f Binary files /dev/null and b/test-data/visual-tests/effects-and-annotations/sustain-600.png differ diff --git a/test-data/visual-tests/effects-and-annotations/sustain-850.png b/test-data/visual-tests/effects-and-annotations/sustain-850.png new file mode 100644 index 000000000..dacd4a0c1 Binary files /dev/null and b/test-data/visual-tests/effects-and-annotations/sustain-850.png differ diff --git a/test-data/visual-tests/effects-and-annotations/sustain.gp b/test-data/visual-tests/effects-and-annotations/sustain.gp new file mode 100644 index 000000000..8b2335ec9 Binary files /dev/null and b/test-data/visual-tests/effects-and-annotations/sustain.gp differ diff --git a/test/visualTests/features/EffectsAndAnnotations.test.ts b/test/visualTests/features/EffectsAndAnnotations.test.ts index 190bd242f..c4c7454cb 100644 --- a/test/visualTests/features/EffectsAndAnnotations.test.ts +++ b/test/visualTests/features/EffectsAndAnnotations.test.ts @@ -25,7 +25,7 @@ describe('EffectsAndAnnotationsTests', () => { }); it('chords-duplicates', async () => { - // This file was manually modified to contain 2 separate chords with the same details. + // This file was manually modified to contain 2 separate chords with the same details. await VisualTestHelper.runVisualTest('effects-and-annotations/chords-duplicates.gp'); }); @@ -44,8 +44,7 @@ describe('EffectsAndAnnotationsTests', () => { it('fade-in', async () => { // quadratic curve rendering in SkiaSharp is edgy with m80, // tolerance compensates this - await VisualTestHelper.runVisualTest('effects-and-annotations/fade-in.gp', - undefined, undefined, undefined, 2); + await VisualTestHelper.runVisualTest('effects-and-annotations/fade-in.gp', undefined, undefined, undefined, 2); }); it('let-ring', async () => { @@ -85,8 +84,9 @@ describe('EffectsAndAnnotationsTests', () => { importer.init(ByteBuffer.fromString(tex), settings); let score = importer.readScore(); - await VisualTestHelper.runVisualTestScoreWithResize(score, - [400], + await VisualTestHelper.runVisualTestScoreWithResize( + score, + [400], ['effects-and-annotations/slides-line-break.png'], settings ); @@ -105,7 +105,7 @@ describe('EffectsAndAnnotationsTests', () => { }); it('tuplets-advanced', async () => { - await VisualTestHelper.runVisualTest('effects-and-annotations/tuplets-advanced.gp', undefined, [0,1,2]); + await VisualTestHelper.runVisualTest('effects-and-annotations/tuplets-advanced.gp', undefined, [0, 1, 2]); }); it('fingering', async () => { @@ -135,4 +135,41 @@ describe('EffectsAndAnnotationsTests', () => { it('beat-slash', async () => { await VisualTestHelper.runVisualTest('effects-and-annotations/beat-slash.gp'); }); + + it('sustain-pedal', async () => { + await VisualTestHelper.runVisualTestWithResize('effects-and-annotations/sustain.gp', [1200, 850, 600], + [ + 'effects-and-annotations/sustain-1200.png', + 'effects-and-annotations/sustain-850.png', + 'effects-and-annotations/sustain-600.png' + ], + ); + }); + + it('sustain-pedal-alphatex', async () => { + const importer = new AlphaTexImporter(); + const settings = new Settings(); + importer.initFromString(` + . + \\track "pno." + :8 G4 { spd } G4 G4 { spu } G4 G4 { spd } G4 {spu} G4 G4 {spd} | + G4 { spu } G4 G4 G4 G4 G4 G4 G4 | + F5.1 { spd } | F5 | F5 | + F5.4 F5.4 { spu } F5 F5 | + G4.8 { spd } G4 G4 { spu } G4 G4 { spd } G4 G4 G4 {spu} | + G4.4 G4.4 G4.4 {spd} G4.4 {spe} + `, settings); + const score = importer.readScore(); + score.stylesheet.hideDynamics = true; + + await VisualTestHelper.runVisualTestScoreWithResize(score, [1200, 850, 600], + [ + 'effects-and-annotations/sustain-1200.png', + 'effects-and-annotations/sustain-850.png', + 'effects-and-annotations/sustain-600.png' + ], + ); + }); + + });