From 23d655ee1a906de216399b3f43001227a4a657ac Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sat, 24 Aug 2024 14:45:12 +0200 Subject: [PATCH 1/3] feat: add free time --- src/Environment.ts | 2 + src/NotationSettings.ts | 9 ++- src/generated/model/MasterBarSerializer.ts | 4 ++ src/importer/AlphaTexImporter.ts | 17 +++-- src/importer/GpifParser.ts | 3 + src/model/MasterBar.ts | 7 +- src/rendering/NumberedBarRenderer.ts | 8 ++- src/rendering/ScoreBarRenderer.ts | 11 ++- src/rendering/SlashBarRenderer.ts | 8 ++- src/rendering/TabBarRenderer.ts | 9 ++- src/rendering/effects/FreeTimeEffectInfo.ts | 45 ++++++++++++ src/rendering/glyphs/BarSeperatorGlyph.ts | 35 +++++++++- src/rendering/glyphs/TimeSignatureGlyph.ts | 64 ++++++++++++------ .../effects-and-annotations/free-time.gp | Bin 0 -> 12743 bytes .../effects-and-annotations/free-time.png | Bin 0 -> 14349 bytes .../features/EffectsAndAnnotations.test.ts | 4 ++ 16 files changed, 182 insertions(+), 44 deletions(-) create mode 100644 src/rendering/effects/FreeTimeEffectInfo.ts create mode 100644 test-data/visual-tests/effects-and-annotations/free-time.gp create mode 100644 test-data/visual-tests/effects-and-annotations/free-time.png diff --git a/src/Environment.ts b/src/Environment.ts index cd9f676b3..b4a45b294 100644 --- a/src/Environment.ts +++ b/src/Environment.ts @@ -62,6 +62,7 @@ import { Settings } from './Settings'; import { AlphaTabError, AlphaTabErrorType } from './AlphaTabError'; import { SlashBarRendererFactory } from './rendering/SlashBarRendererFactory'; import { NumberedBarRendererFactory } from './rendering/NumberedBarRendererFactory'; +import { FreeTimeEffectInfo } from './rendering/effects/FreeTimeEffectInfo'; export class LayoutEngineFactory { public readonly vertical: boolean; @@ -484,6 +485,7 @@ export class Environment { new TempoEffectInfo(), new TripletFeelEffectInfo(), new MarkerEffectInfo(), + new FreeTimeEffectInfo(), new TextEffectInfo(), new ChordsEffectInfo() ]), diff --git a/src/NotationSettings.ts b/src/NotationSettings.ts index 10830720d..0099886c1 100644 --- a/src/NotationSettings.ts +++ b/src/NotationSettings.ts @@ -127,7 +127,7 @@ export enum NotationElement { * The track names which are shown in the accolade. */ TrackNames, - + /** * The chord diagrams for guitars. Usually shown * below the score info. @@ -287,7 +287,12 @@ export enum NotationElement { /** * The left hand tap symbol shown above the staff. */ - EffectLeftHandTap + EffectLeftHandTap, + + /** + * The "Free time" text shown above the staff. + */ + EffectFreeTime } /** diff --git a/src/generated/model/MasterBarSerializer.ts b/src/generated/model/MasterBarSerializer.ts index dffaa721b..3edab011e 100644 --- a/src/generated/model/MasterBarSerializer.ts +++ b/src/generated/model/MasterBarSerializer.ts @@ -35,6 +35,7 @@ export class MasterBarSerializer { o.set("timesignaturenumerator", obj.timeSignatureNumerator); o.set("timesignaturedenominator", obj.timeSignatureDenominator); o.set("timesignaturecommon", obj.timeSignatureCommon); + o.set("isfreetime", obj.isFreeTime); o.set("tripletfeel", obj.tripletFeel as number); o.set("section", SectionSerializer.toJson(obj.section)); o.set("tempoautomations", obj.tempoAutomations.map(i => AutomationSerializer.toJson(i))); @@ -80,6 +81,9 @@ export class MasterBarSerializer { case "timesignaturecommon": obj.timeSignatureCommon = v! as boolean; return true; + case "isfreetime": + obj.isFreeTime = v! as boolean; + return true; case "tripletfeel": obj.tripletFeel = JsonHelper.parseEnum(v, TripletFeel)!; return true; diff --git a/src/importer/AlphaTexImporter.ts b/src/importer/AlphaTexImporter.ts index 8926b1cfe..f48546b80 100644 --- a/src/importer/AlphaTexImporter.ts +++ b/src/importer/AlphaTexImporter.ts @@ -885,7 +885,10 @@ export class AlphaTexImporter extends ScoreImporter { if (name === 'defaults') { for (const [defaultName, defaultValue] of PercussionMapper.instrumentArticulationNames) { this._percussionArticulationNames.set(defaultName.toLowerCase(), defaultValue); - this._percussionArticulationNames.set(AlphaTexImporter.toArticulationId(defaultName), defaultValue); + this._percussionArticulationNames.set( + AlphaTexImporter.toArticulationId(defaultName), + defaultValue + ); } return true; } @@ -910,12 +913,12 @@ export class AlphaTexImporter extends ScoreImporter { return false; } } - + /** * Encodes a given string to a shorthand text form without spaces or special characters */ private static toArticulationId(plain: string): string { - return plain.replace(new RegExp("[^a-zA-Z0-9]", "g"), "").toLowerCase() + return plain.replace(new RegExp('[^a-zA-Z0-9]', 'g'), '').toLowerCase(); } private applyPercussionStaff(staff: Staff) { @@ -1215,7 +1218,7 @@ export class AlphaTexImporter extends ScoreImporter { } private beat(voice: Voice): boolean { - // duration specifier? + // duration specifier? this.beatDuration(); let beat: Beat = new Beat(); @@ -1506,7 +1509,7 @@ export class AlphaTexImporter extends ScoreImporter { beat.crescendo = CrescendoType.Crescendo; } else if (syData === 'dec') { beat.crescendo = CrescendoType.Decrescendo; - } else if(syData === 'tempo') { + } else if (syData === 'tempo') { // NOTE: playbackRatio is calculated on score finish when playback positions are known const tempoAutomation = this.readTempoAutomation(); beat.automations.push(tempoAutomation); @@ -1604,7 +1607,7 @@ export class AlphaTexImporter extends ScoreImporter { fret = this._syData as number; if (this._currentStaff.isPercussion && !PercussionMapper.instrumentArticulations.has(fret)) { this.errorMessage(`Unknown percussion articulation ${fret}`); - } + } break; case AlphaTexSymbols.String: if (this._currentStaff.isPercussion) { @@ -1929,6 +1932,8 @@ export class AlphaTexImporter extends ScoreImporter { } master.timeSignatureDenominator = this._syData as number; this._sy = this.newSy(); + } else if (syData == 'ft') { + master.isFreeTime = true; } else if (syData === 'ro') { master.isRepeatStart = true; this._sy = this.newSy(); diff --git a/src/importer/GpifParser.ts b/src/importer/GpifParser.ts index 5f1619b18..fcb73f47e 100644 --- a/src/importer/GpifParser.ts +++ b/src/importer/GpifParser.ts @@ -1157,6 +1157,9 @@ export class GpifParser { masterBar.timeSignatureNumerator = parseInt(timeParts[0]); masterBar.timeSignatureDenominator = parseInt(timeParts[1]); break; + case 'FreeTime': + masterBar.isFreeTime = true; + break; case 'DoubleBar': masterBar.isDoubleBar = true; break; diff --git a/src/model/MasterBar.ts b/src/model/MasterBar.ts index ef9011a4c..425054b0d 100644 --- a/src/model/MasterBar.ts +++ b/src/model/MasterBar.ts @@ -91,6 +91,11 @@ export class MasterBar { */ public timeSignatureCommon: boolean = false; + /** + * Gets or sets whether the bar indicates a free time playing. + */ + public isFreeTime: boolean = false; + /** * Gets or sets the triplet feel that is valid for this bar. */ @@ -148,7 +153,7 @@ export class MasterBar { /** * An absolute width of the bar to use when displaying in a multi-track layout. */ - public displayWidth:number = -1; + public displayWidth: number = -1; /** * Calculates the time spent in this bar. (unit: midi ticks) diff --git a/src/rendering/NumberedBarRenderer.ts b/src/rendering/NumberedBarRenderer.ts index 49c1fae4e..215467534 100644 --- a/src/rendering/NumberedBarRenderer.ts +++ b/src/rendering/NumberedBarRenderer.ts @@ -259,13 +259,15 @@ export class NumberedBarRenderer extends LineBarRenderer { private createTimeSignatureGlyphs(): void { this.addPreBeatGlyph(new SpacingGlyph(0, 0, 5 * this.scale)); + const masterBar = this.bar.masterBar; this.addPreBeatGlyph( new ScoreTimeSignatureGlyph( 0, this.getLineY(0), - this.bar.masterBar.timeSignatureNumerator, - this.bar.masterBar.timeSignatureDenominator, - this.bar.masterBar.timeSignatureCommon + masterBar.timeSignatureNumerator, + masterBar.timeSignatureDenominator, + masterBar.timeSignatureCommon, + masterBar.isFreeTime && masterBar.previousMasterBar == null || masterBar.isFreeTime !== masterBar.previousMasterBar!.isFreeTime, ) ); } diff --git a/src/rendering/ScoreBarRenderer.ts b/src/rendering/ScoreBarRenderer.ts index f3ffbd039..f91f1beeb 100644 --- a/src/rendering/ScoreBarRenderer.ts +++ b/src/rendering/ScoreBarRenderer.ts @@ -390,7 +390,11 @@ export class ScoreBarRenderer extends LineBarRenderer { (this.bar.previousBar && this.bar.masterBar.timeSignatureNumerator !== this.bar.previousBar.masterBar.timeSignatureNumerator) || (this.bar.previousBar && - this.bar.masterBar.timeSignatureDenominator !== this.bar.previousBar.masterBar.timeSignatureDenominator) + this.bar.masterBar.timeSignatureDenominator !== + this.bar.previousBar.masterBar.timeSignatureDenominator) || + (this.bar.previousBar && + this.bar.masterBar.isFreeTime && + this.bar.masterBar.isFreeTime !== this.bar.previousBar.masterBar.isFreeTime) ) { this.createStartSpacing(); this.createTimeSignatureGlyphs(); @@ -470,7 +474,8 @@ export class ScoreBarRenderer extends LineBarRenderer { this.getScoreY(lines), this.bar.masterBar.timeSignatureNumerator, this.bar.masterBar.timeSignatureDenominator, - this.bar.masterBar.timeSignatureCommon + this.bar.masterBar.timeSignatureCommon, + this.bar.masterBar.isFreeTime, ) ); } @@ -527,4 +532,4 @@ export class ScoreBarRenderer extends LineBarRenderer { canvas.stroke(); canvas.lineWidth = this.scale; } -} \ No newline at end of file +} diff --git a/src/rendering/SlashBarRenderer.ts b/src/rendering/SlashBarRenderer.ts index 867729f9a..634415696 100644 --- a/src/rendering/SlashBarRenderer.ts +++ b/src/rendering/SlashBarRenderer.ts @@ -114,13 +114,15 @@ export class SlashBarRenderer extends LineBarRenderer { private createTimeSignatureGlyphs(): void { this.addPreBeatGlyph(new SpacingGlyph(0, 0, 5 * this.scale)); + const masterBar = this.bar.masterBar; this.addPreBeatGlyph( new ScoreTimeSignatureGlyph( 0, this.getLineY(0), - this.bar.masterBar.timeSignatureNumerator, - this.bar.masterBar.timeSignatureDenominator, - this.bar.masterBar.timeSignatureCommon + masterBar.timeSignatureNumerator, + masterBar.timeSignatureDenominator, + masterBar.timeSignatureCommon, + masterBar.isFreeTime && masterBar.previousMasterBar == null || masterBar.isFreeTime !== masterBar.previousMasterBar!.isFreeTime, ) ); } diff --git a/src/rendering/TabBarRenderer.ts b/src/rendering/TabBarRenderer.ts index d5a296980..998e92591 100644 --- a/src/rendering/TabBarRenderer.ts +++ b/src/rendering/TabBarRenderer.ts @@ -129,7 +129,10 @@ export class TabBarRenderer extends LineBarRenderer { this.bar.previousBar.masterBar.timeSignatureNumerator) || (this.bar.previousBar && this.bar.masterBar.timeSignatureDenominator !== - this.bar.previousBar.masterBar.timeSignatureDenominator)) + this.bar.previousBar.masterBar.timeSignatureDenominator) || + (this.bar.previousBar && + this.bar.masterBar.isFreeTime && + this.bar.masterBar.isFreeTime !== this.bar.previousBar.masterBar.isFreeTime)) ) { this.createStartSpacing(); this.createTimeSignatureGlyphs(); @@ -146,7 +149,9 @@ export class TabBarRenderer extends LineBarRenderer { this.getTabY(lines), this.bar.masterBar.timeSignatureNumerator, this.bar.masterBar.timeSignatureDenominator, - this.bar.masterBar.timeSignatureCommon + this.bar.masterBar.timeSignatureCommon, + this.bar.masterBar.isFreeTime, + ) ); } diff --git a/src/rendering/effects/FreeTimeEffectInfo.ts b/src/rendering/effects/FreeTimeEffectInfo.ts new file mode 100644 index 000000000..300df46a3 --- /dev/null +++ b/src/rendering/effects/FreeTimeEffectInfo.ts @@ -0,0 +1,45 @@ +import { Beat } from '@src/model/Beat'; +import { TextAlign } from '@src/platform/ICanvas'; +import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import { EffectBarGlyphSizing } from '@src/rendering/EffectBarGlyphSizing'; +import { EffectGlyph } from '@src/rendering/glyphs/EffectGlyph'; +import { TextGlyph } from '@src/rendering/glyphs/TextGlyph'; +import { EffectBarRendererInfo } from '@src/rendering/EffectBarRendererInfo'; +import { Settings } from '@src/Settings'; +import { NotationElement } from '@src/NotationSettings'; + +export class FreeTimeEffectInfo extends EffectBarRendererInfo { + public get notationElement(): NotationElement { + return NotationElement.EffectText; + } + + public get hideOnMultiTrack(): boolean { + return false; + } + + public get canShareBand(): boolean { + return true; + } + + public get sizingMode(): EffectBarGlyphSizing { + return EffectBarGlyphSizing.SinglePreBeat; + } + + public shouldCreateGlyph(settings: Settings, beat: Beat): boolean { + const masterBar = beat.voice.bar.masterBar; + const isFirstBeat = beat.voice.bar.staff.index === 0 && beat.voice.index === 0 && beat.index === 0; + return ( + isFirstBeat && + masterBar.isFreeTime && + (masterBar.index === 0 || masterBar.isFreeTime != masterBar.previousMasterBar!.isFreeTime) + ); + } + + public createNewGlyph(renderer: BarRendererBase, beat: Beat): EffectGlyph { + return new TextGlyph(0, 0, 'Free time', renderer.resources.effectFont, TextAlign.Left); + } + + public canExpand(from: Beat, to: Beat): boolean { + return true; + } +} diff --git a/src/rendering/glyphs/BarSeperatorGlyph.ts b/src/rendering/glyphs/BarSeperatorGlyph.ts index 20256656b..7c255c570 100644 --- a/src/rendering/glyphs/BarSeperatorGlyph.ts +++ b/src/rendering/glyphs/BarSeperatorGlyph.ts @@ -2,6 +2,8 @@ import { ICanvas } from '@src/platform/ICanvas'; import { Glyph } from '@src/rendering/glyphs/Glyph'; export class BarSeperatorGlyph extends Glyph { + private static readonly DashSize: number = 4; + public constructor(x: number, y: number) { super(x, y); } @@ -40,9 +42,36 @@ export class BarSeperatorGlyph extends Glyph { !this.renderer.nextRenderer.bar.masterBar.isRepeatStart ) { // small bar - canvas.fillRect(left + this.width - this.scale, top, this.scale, h); - if (this.renderer.bar.masterBar.isDoubleBar) { - canvas.fillRect(left + this.width - 5 * this.scale, top, this.scale, h); + if (this.renderer.bar.masterBar.isFreeTime) { + const dashSize: number = BarSeperatorGlyph.DashSize * this.scale; + const x = ((left + this.width - this.scale) | 0) + 0.5; + const bottom = top + h; + + let dashes: number = Math.ceil(h / 2 / dashSize); + + canvas.beginPath(); + if (dashes < 1) { + canvas.moveTo(x, top); + canvas.lineTo(x, bottom); + } else { + let dashY = top; + + // spread the dashes so they complete directly on the end-Y + const freeSpace = h - dashes * dashSize; + const freeSpacePerDash = freeSpace / (dashes - 1); + + while (dashY < bottom) { + canvas.moveTo(x, dashY); + canvas.lineTo(x, dashY + dashSize); + dashY += dashSize + freeSpacePerDash; + } + } + canvas.stroke(); + } else { + canvas.fillRect(left + this.width - this.scale, top, this.scale, h); + if (this.renderer.bar.masterBar.isDoubleBar) { + canvas.fillRect(left + this.width - 5 * this.scale, top, this.scale, h); + } } } } diff --git a/src/rendering/glyphs/TimeSignatureGlyph.ts b/src/rendering/glyphs/TimeSignatureGlyph.ts index d59385b56..74617f878 100644 --- a/src/rendering/glyphs/TimeSignatureGlyph.ts +++ b/src/rendering/glyphs/TimeSignatureGlyph.ts @@ -1,56 +1,78 @@ -import { Glyph } from '@src/rendering/glyphs/Glyph'; import { GlyphGroup } from '@src/rendering/glyphs/GlyphGroup'; import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; import { NumberGlyph } from '@src/rendering/glyphs/NumberGlyph'; +import { GhostParenthesisGlyph } from './GhostParenthesisGlyph'; export abstract class TimeSignatureGlyph extends GlyphGroup { private _numerator: number = 0; private _denominator: number = 0; private _isCommon: boolean; + private _isFreeTime: boolean; - public constructor(x: number, y: number, numerator: number, denominator: number, isCommon: boolean) { + public constructor( + x: number, + y: number, + numerator: number, + denominator: number, + isCommon: boolean, + isFreeTime: boolean + ) { super(x, y); this._numerator = numerator; this._denominator = denominator; this._isCommon = isCommon; + this._isFreeTime = isFreeTime; } protected abstract get commonScale(): number; protected abstract get numberScale(): number; + public override doLayout(): void { + let x = 0; + const numberHeight = NumberGlyph.numberHeight * this.scale; + if (this._isFreeTime) { + const g = new GhostParenthesisGlyph(true); + g.renderer = this.renderer; + g.y = -numberHeight; + g.height = numberHeight * 2; + g.doLayout(); + this.addGlyph(g); + x += g.width + 10 * this.scale; + } + if (this._isCommon && this._numerator === 2 && this._denominator === 2) { - let common: MusicFontGlyph = new MusicFontGlyph( - 0, - 0, - this.commonScale, - MusicFontSymbol.TimeSigCutCommon - ); + let common: MusicFontGlyph = new MusicFontGlyph(x, 0, this.commonScale, MusicFontSymbol.TimeSigCutCommon); common.width = 14 * this.scale; this.addGlyph(common); super.doLayout(); } else if (this._isCommon && this._numerator === 4 && this._denominator === 4) { - let common: MusicFontGlyph = new MusicFontGlyph( - 0, - 0, - this.commonScale, - MusicFontSymbol.TimeSigCommon - ); + let common: MusicFontGlyph = new MusicFontGlyph(x, 0, this.commonScale, MusicFontSymbol.TimeSigCommon); common.width = 14 * this.scale; this.addGlyph(common); super.doLayout(); } else { - const numberHeight = NumberGlyph.numberHeight * this.scale; - let numerator: NumberGlyph = new NumberGlyph(0, -numberHeight / 2, this._numerator, this.numberScale); - let denominator: NumberGlyph = new NumberGlyph(0, numberHeight / 2, this._denominator, this.numberScale); + let numerator: NumberGlyph = new NumberGlyph(x, -numberHeight / 2, this._numerator, this.numberScale); + let denominator: NumberGlyph = new NumberGlyph(x, numberHeight / 2, this._denominator, this.numberScale); this.addGlyph(numerator); this.addGlyph(denominator); super.doLayout(); - for (let i: number = 0, j: number = this.glyphs!.length; i < j; i++) { - let g: Glyph = this.glyphs![i]; - g.x = (this.width - g.width) / 2; - } + + const glyphSpace = this.width - x; + numerator.x = x + (glyphSpace - numerator.width) / 2; + denominator.x = x + (glyphSpace - denominator.width) / 2; + } + + if (this._isFreeTime) { + const g = new GhostParenthesisGlyph(false); + g.renderer = this.renderer; + g.x = this.width + 13 * this.scale; + g.y = -numberHeight; + g.height = numberHeight * 2; + g.doLayout(); + this.addGlyph(g); + this.width = g.x + g.width; } } } diff --git a/test-data/visual-tests/effects-and-annotations/free-time.gp b/test-data/visual-tests/effects-and-annotations/free-time.gp new file mode 100644 index 0000000000000000000000000000000000000000..21d15315a54f2f27478d5a908c426ff1097013fb GIT binary patch literal 12743 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 literal 0 HcmV?d00001 diff --git a/test-data/visual-tests/effects-and-annotations/free-time.png b/test-data/visual-tests/effects-and-annotations/free-time.png new file mode 100644 index 0000000000000000000000000000000000000000..1f7cb721f5201a56cb89ca7287875095d7433eeb GIT binary patch literal 14349 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 { it('triplet-feel', async () => { await VisualTestHelper.runVisualTest('effects-and-annotations/triplet-feel.gp'); }); + + it('free-time', async () => { + await VisualTestHelper.runVisualTest('effects-and-annotations/free-time.gp'); + }); }); From 6c3a86be58bef1e28206d0a06500f29591387afc Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sat, 24 Aug 2024 14:49:21 +0200 Subject: [PATCH 2/3] feat: add free time to exporter --- src/exporter/GpifWriter.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/exporter/GpifWriter.ts b/src/exporter/GpifWriter.ts index b0ccbd9c2..2d2ff9e8e 100644 --- a/src/exporter/GpifWriter.ts +++ b/src/exporter/GpifWriter.ts @@ -1548,6 +1548,12 @@ export class GpifWriter { 'Time' ).innerText = `${masterBar.timeSignatureNumerator}/${masterBar.timeSignatureDenominator}`; + if(masterBar.isFreeTime) { + masterBarNode.addElement( + 'FreeTime' + ); + } + let bars: string[] = []; for (const tracks of masterBar.score.tracks) { for (const staves of tracks.staves) { From 53eb52782caa1be7ae973f10433517b8b66c6207 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sat, 24 Aug 2024 14:53:34 +0200 Subject: [PATCH 3/3] fix: cleanup duplicated vars --- src/rendering/glyphs/BarSeperatorGlyph.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/rendering/glyphs/BarSeperatorGlyph.ts b/src/rendering/glyphs/BarSeperatorGlyph.ts index 7c255c570..b2b01f6f0 100644 --- a/src/rendering/glyphs/BarSeperatorGlyph.ts +++ b/src/rendering/glyphs/BarSeperatorGlyph.ts @@ -45,9 +45,7 @@ export class BarSeperatorGlyph extends Glyph { if (this.renderer.bar.masterBar.isFreeTime) { const dashSize: number = BarSeperatorGlyph.DashSize * this.scale; const x = ((left + this.width - this.scale) | 0) + 0.5; - const bottom = top + h; - - let dashes: number = Math.ceil(h / 2 / dashSize); + const dashes: number = Math.ceil(h / 2 / dashSize); canvas.beginPath(); if (dashes < 1) {