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/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) { 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..b2b01f6f0 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,34 @@ 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 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 000000000..21d15315a Binary files /dev/null and b/test-data/visual-tests/effects-and-annotations/free-time.gp differ 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 000000000..1f7cb721f Binary files /dev/null and b/test-data/visual-tests/effects-and-annotations/free-time.png differ diff --git a/test/visualTests/features/EffectsAndAnnotations.test.ts b/test/visualTests/features/EffectsAndAnnotations.test.ts index 497bd06ce..c0031dc97 100644 --- a/test/visualTests/features/EffectsAndAnnotations.test.ts +++ b/test/visualTests/features/EffectsAndAnnotations.test.ts @@ -115,4 +115,8 @@ describe('EffectsAndAnnotationsTests', () => { 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'); + }); });