src/Domain/Guitar.ts
import { FretPrimitives, GuitarStringPrimitives, PositionPrimitives } from '../primitives/Guitar';
import { PitchLines } from './Barry';
import { Chord } from './Chord';
import { MelodicLine, Octave } from './Note';
import { Pitch, PitchLine, PitchLineDirection } from './Pitch';
export class TabColumn {
private readonly maxRowLength: number = 0;
private constructor(private readonly rows: string[]) {
this.maxRowLength = Math.max(...rows.map((r) => r.length));
}
public static readonly Start: TabColumn = new TabColumn(Array<string>(6).fill('|-'));
public static readonly Bar: TabColumn = new TabColumn(Array<string>(6).fill('-|-'));
public static readonly End: TabColumn = new TabColumn(Array<string>(6).fill('-|'));
public static readonly Rest: TabColumn = new TabColumn(Array<string>(6).fill(`-`));
public static readonly Separator: TabColumn = new TabColumn(Array<string>(6).fill(`-`));
public static readonly StandardTunning: TabColumn = new TabColumn(['e', 'B', 'G', 'D', 'A', 'E']);
render(): string[] {
return this.rows.map((r) => (r.length < this.maxRowLength ? `-${r}` : r));
}
static fromFrets(frets: Fret[]): TabColumn {
return new TabColumn(frets.map((f) => f.toString()));
}
}
export class Fret {
constructor(
protected readonly string: GuitarString,
private readonly fret: number,
private readonly pitch?: Pitch,
private readonly octave?: Octave
) {}
get Number(): number {
return this.fret;
}
get String(): GuitarString {
return this.string;
}
get Pitch() {
return this.pitch;
}
get Octave() {
return this.octave;
}
get To(): FretPrimitives {
return {
string: this.String.To,
fret: this.Number,
};
}
equals(other: Fret): boolean {
return this.fret === other.fret && this.string.equals(other.string);
}
raiseOctave(): Fret {
return new Fret(this.string, this.fret + 12, this.pitch, this.octave?.up());
}
isWithin(lowFret: Fret, highFret: Fret, lowerMargin = 0, higherMargin = 0) {
return (
(this.isHigher(lowFret, lowerMargin) || this.isSameFretNumber(lowFret)) &&
(this.isLower(highFret, higherMargin) || this.isSameFretNumber(highFret))
);
}
toTab(guitarStrings: GuitarStrings = new GuitarStrings()): TabColumn {
const frets: Fret[] = [...guitarStrings].map((gs) =>
this.String.equals(gs) ? this : new BlankFret(gs)
);
return TabColumn.fromFrets([...frets].reverse());
}
toString() {
return this.Number.toString();
}
isOnSameStringAs(other: Fret) {
return this.String == other.String;
}
isOnAdjacentStringAs(other: Fret) {
return this.String == other.String.NextAscending || this.String == other.String.NextDescending;
}
isSkipingASingleStringAs(other: Fret) {
const nextNextAscending = other.String.NextAscending.NextAscending;
const nextNextDescending = other.String.NextDescending.NextDescending;
return this.String == nextNextAscending || this.String == nextNextDescending;
}
private isHigher(other: Fret, margin: number) {
return this.fret + margin > other.fret;
}
private isLower(other: Fret, margin: number) {
return this.fret - margin < other.fret;
}
private isSameFretNumber(other: Fret) {
return this.fret === other.fret;
}
}
export class BlankFret extends Fret {
constructor(string: GuitarString = GuitarString.Sixth, fret = -1) {
super(string, fret);
}
override toTab(guitarStrings: GuitarStrings = new GuitarStrings()): TabColumn {
const frets: Fret[] = [...guitarStrings].map((gs) => new BlankFret(gs));
return TabColumn.fromFrets([...frets]);
}
override toString() {
return '-';
}
override raiseOctave(): Fret {
return new BlankFret(this.string, -1);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
override isWithin(_lowFret: Fret, _highFret: Fret, _lowerMargin = 0, _higherMargin = 0) {
return false;
}
}
abstract class Frets implements Iterable<Fret> {
protected frets: Fret[] = [];
protected constructor(frets: Fret[] = []) {
this.frets = frets;
}
*[Symbol.iterator](): Iterator<Fret> {
for (const f of this.frets) {
yield f;
}
}
reverse() {
this.frets.reverse();
}
protected areAllFretsOnSameGuitarString() {
return this.frets.every((f) => f.isOnSameStringAs(this.frets[0]!));
}
protected areAllFretsWithinAdjacentGuitarStrings() {
return this.frets.every(
(f) => f.isOnSameStringAs(this.frets[0]!) || f.isOnAdjacentStringAs(this.frets[0]!)
);
}
protected areAllFretsSkipingASinleString() {
return this.frets.every(
(f) =>
f.isOnSameStringAs(this.frets[0]!) ||
f.isOnAdjacentStringAs(this.frets[0]!) ||
f.isSkipingASingleStringAs(this.frets[0]!)
);
}
}
class VerticalFrets extends Frets {
constructor(frets: Fret[] = Array<Fret>(6).fill(new BlankFret())) {
super(frets);
}
updateFretAt(fret: Fret, position: number) {
this.frets[position] = fret;
}
adjustOpenStringOctaves() {
if (
this.frets.some((f) => f.Number === Position.Open.Low) &&
this.frets.some((f) => f.Number >= Position.Open.High)
) {
this.frets = this.frets.map((f) => (f.Number === 0 ? f.raiseOctave() : f));
}
}
isTooFar(fret: Fret): boolean {
return this.frets
.filter((f) => f.Number !== -1)
.some((f) => Math.abs(f.Number - fret.Number) >= 4);
}
override toString(): string {
return this.frets.map((f) => f.toString()).join('\n');
}
toTab(): TabColumn {
return TabColumn.fromFrets([...this.frets]);
}
}
export class HorizontalFrets extends Frets {
constructor(frets: Fret[] = []) {
super(frets);
}
push(fret: Fret) {
this.frets.push(fret);
}
concat(frets: HorizontalFrets) {
this.frets = this.frets.concat(frets.frets);
}
last() {
return this.frets[this.frets.length - 1];
}
toTab(guitarStrings: GuitarStrings): Tab {
const column = this.frets.map((fret) => fret.toTab(guitarStrings));
return new Tab(...column);
}
smoothness() {
if (this.areAllFretsOnSameGuitarString()) {
return 0;
}
if (this.areAllFretsWithinAdjacentGuitarStrings()) {
return 1;
}
if (this.areAllFretsSkipingASinleString()) {
return 2;
}
return 3;
}
get length() {
return this.frets.length;
}
}
export class GuitarStrings implements Iterable<GuitarString> {
constructor(private readonly guitarStrings: GuitarString[] = GuitarString.standardTunning) {}
toTunning(tunning: GuitarTuning) {
return new GuitarStrings(this.guitarStrings.map((gs) => gs.toTunning(tunning)));
}
guitarString(guitarStringIndex: number) {
return this.guitarStrings.find((gs) => gs.Index == guitarStringIndex)!;
}
lowerToHigher() {
return new GuitarStrings(
this.guitarStrings.sort((s1: GuitarString, s2: GuitarString) => s1.Index - s2.Index)
);
}
sortByDirectionAndString(lineDirection: PitchLineDirection, previousString: GuitarString) {
return lineDirection === PitchLineDirection.Descending
? this.higherThan(previousString).lowerToHigher()
: this.lowerThan(previousString);
}
*[Symbol.iterator](): Iterator<GuitarString> {
for (const guitarString of this.guitarStrings) {
yield guitarString;
}
}
private higherThan(guitarString: GuitarString) {
return new GuitarStrings(this.guitarStrings.filter((s) => !s.isLowerIndexThan(guitarString)));
}
private lowerThan(guitarString: GuitarString) {
return new GuitarStrings(this.guitarStrings.filter((s) => !s.isHigherIndexThan(guitarString)));
}
}
export class GuitarString {
private static readonly all: GuitarString[] = [];
private constructor(
private readonly name: string,
private openStringPitch: Pitch,
private openStringOctave: Octave,
private readonly index: number,
private readonly nextAscending: () => GuitarString,
private readonly nextDescending: () => GuitarString
) {
GuitarString.all.push(this);
}
get To(): GuitarStringPrimitives {
return {
name: this.name,
index: this.index,
};
}
public static readonly Sixth: GuitarString = new GuitarString(
'Sixth',
Pitch.E,
Octave.C2,
6,
() => GuitarString.Fifth,
() => GuitarString.Sixth
);
public static readonly Fifth: GuitarString = new GuitarString(
'Fifth',
Pitch.A,
Octave.C2,
5,
() => GuitarString.Fourth,
() => GuitarString.Sixth
);
public static readonly Fourth: GuitarString = new GuitarString(
'Fourth',
Pitch.D,
Octave.C3,
4,
() => GuitarString.Third,
() => GuitarString.Fifth
);
public static readonly Third: GuitarString = new GuitarString(
'Third',
Pitch.G,
Octave.C3,
3,
() => GuitarString.Second,
() => GuitarString.Fourth
);
public static readonly Second: GuitarString = new GuitarString(
'Second',
Pitch.B,
Octave.C3,
2,
() => GuitarString.First,
() => GuitarString.Third
);
public static readonly First: GuitarString = new GuitarString(
'First',
Pitch.E,
Octave.C4,
1,
() => GuitarString.First,
() => GuitarString.Second
);
static get standardTunning(): GuitarString[] {
return GuitarString.all;
}
toTunning(tunning: GuitarTuning) {
return new GuitarString(
this.name,
tunning.openStringPitchFor(this),
this.openStringOctave,
this.Index,
() => this.NextDescending,
() => this.NextAscending
);
}
fretFor(pitch: Pitch): Fret {
return new Fret(this, this.openStringPitch.absoluteDistance(pitch), pitch);
}
fretsFor(position: Position) {
return new HorizontalFrets(this.fretsFromTo(position.Low, position.High));
}
fretsFromTo(from: number, to: number) {
const frets: Fret[] = [];
let pitch = this.openStringPitch;
let octave = this.openStringOctave;
for (let i = from; i--; ) {
if (pitch == Pitch.C) {
octave = octave.up();
}
pitch = pitch.sharp();
}
for (let i = from; i <= to; i++) {
if (pitch == Pitch.C) {
octave = octave.up();
}
frets.push(new Fret(this, i, pitch, octave));
pitch = pitch.sharp();
}
return frets;
}
equals(other: GuitarString): boolean {
return this.index === other.index;
}
isHigherIndexThan(other: GuitarString): boolean {
return other.index < this.index;
}
isLowerIndexThan(other: GuitarString): boolean {
return other.index > this.index;
}
get Index() {
return this.index;
}
get NextAscending(): GuitarString {
return this.nextAscending();
}
get NextDescending(): GuitarString {
return this.nextDescending();
}
}
export class GuitarTuning {
private static readonly all: GuitarTuning[] = [];
private constructor(
private readonly name: string,
private readonly pitches: Pitch[]
) {
GuitarTuning.all.push(this);
}
get Name() {
return this.name;
}
openStringPitchFor(guitarString: GuitarString) {
return this.pitches[guitarString.Index - 1]!;
}
public static readonly OpenA: GuitarTuning = new GuitarTuning(
'Open A',
[Pitch.E, Pitch.A, Pitch.CSharp, Pitch.E, Pitch.A, Pitch.E].reverse()
);
public static readonly OpenAltA: GuitarTuning = new GuitarTuning(
'Open A alternate',
[Pitch.A, Pitch.E, Pitch.A, Pitch.E, Pitch.A, Pitch.CSharp].reverse()
);
public static readonly OpenASlide: GuitarTuning = new GuitarTuning(
'Open A alternate',
[Pitch.E, Pitch.A, Pitch.E, Pitch.A, Pitch.CSharp, Pitch.E].reverse()
);
public static readonly OpenB: GuitarTuning = new GuitarTuning(
'Open B',
[Pitch.B, Pitch.FSharp, Pitch.B, Pitch.FSharp, Pitch.B, Pitch.DSharp].reverse()
);
public static readonly OpenAltB: GuitarTuning = new GuitarTuning(
'Open B alternate',
[Pitch.FSharp, Pitch.B, Pitch.DSharp, Pitch.FSharp, Pitch.B, Pitch.DSharp].reverse()
);
public static readonly OpenC: GuitarTuning = new GuitarTuning(
'Open C',
[Pitch.C, Pitch.G, Pitch.C, Pitch.G, Pitch.C, Pitch.E].reverse()
);
public static readonly OpenAltC: GuitarTuning = new GuitarTuning(
'Open C alternate',
[Pitch.C, Pitch.E, Pitch.G, Pitch.C, Pitch.E, Pitch.G].reverse()
);
public static readonly OpenD: GuitarTuning = new GuitarTuning(
'Open D',
[Pitch.D, Pitch.A, Pitch.D, Pitch.FSharp, Pitch.A, Pitch.D].reverse()
);
public static readonly DropD: GuitarTuning = new GuitarTuning(
'Drop D',
[Pitch.D, Pitch.A, Pitch.D, Pitch.G, Pitch.B, Pitch.E].reverse()
);
public static readonly OpenG: GuitarTuning = new GuitarTuning(
'Open G',
[Pitch.D, Pitch.G, Pitch.D, Pitch.G, Pitch.B, Pitch.D].reverse()
);
public static readonly EFlat: GuitarTuning = new GuitarTuning(
'Eb',
[Pitch.EFlat, Pitch.AFlat, Pitch.DFlat, Pitch.GFlat, Pitch.BFlat, Pitch.EFlat].reverse()
);
public static readonly D: GuitarTuning = new GuitarTuning(
'D',
[Pitch.D, Pitch.G, Pitch.C, Pitch.F, Pitch.A, Pitch.D].reverse()
);
}
export class Position {
private static readonly all: Position[] = [];
private constructor(
private name: string,
private lowFret: Fret,
private highFret: Fret
) {
Position.all.push(this);
}
get High() {
return this.highFret.Number;
}
get Low() {
return this.lowFret.Number;
}
get To(): PositionPrimitives {
return {
name: this.name,
lowestFret: this.lowFret.To,
highestFret: this.highFret.To,
};
}
public static From(primitive: PositionPrimitives) {
const position = Position.all.find(
(p) =>
p.name === primitive.name &&
p.lowFret.Number === primitive.lowestFret.fret &&
p.highFret.Number === primitive.highestFret.fret
);
if (!position) {
throw new Error('Invalid position');
}
return position;
}
public static readonly Open: Position = new Position(
'Open',
new Fret(GuitarString.Sixth, 0, Pitch.E, Octave.C2),
new Fret(GuitarString.First, 4, Pitch.GSharp, Octave.C4)
);
public static readonly C: Position = new Position(
'C',
new Fret(GuitarString.Sixth, 1, Pitch.F, Octave.C2),
new Fret(GuitarString.First, 5, Pitch.A, Octave.C4)
);
public static readonly A: Position = new Position(
'A',
new Fret(GuitarString.Sixth, 4, Pitch.AFlat, Octave.C2),
new Fret(GuitarString.First, 8, Pitch.C, Octave.C5)
);
public static readonly G: Position = new Position(
'G',
new Fret(GuitarString.Sixth, 6, Pitch.BFlat, Octave.C2),
new Fret(GuitarString.First, 10, Pitch.D, Octave.C5)
);
public static readonly E: Position = new Position(
'E',
new Fret(GuitarString.Sixth, 9, Pitch.DFlat, Octave.C3),
new Fret(GuitarString.First, 12, Pitch.E, Octave.C5)
);
public static readonly D: Position = new Position(
'D',
new Fret(GuitarString.Sixth, 11, Pitch.EFlat, Octave.C3),
new Fret(GuitarString.First, 15, Pitch.G, Octave.C5)
);
public static readonly C8: Position = new Position(
'C8',
new Fret(GuitarString.Sixth, 14, Pitch.GFlat, Octave.C3),
new Fret(GuitarString.First, 17, Pitch.A, Octave.C5)
);
public static readonly A8: Position = new Position(
'A8',
new Fret(GuitarString.Sixth, 16, Pitch.AFlat, Octave.C3),
new Fret(GuitarString.First, 20, Pitch.C, Octave.C6)
);
public static readonly G8: Position = new Position(
'G8',
new Fret(GuitarString.Sixth, 18, Pitch.BFlat, Octave.C3),
new Fret(GuitarString.First, 22, Pitch.D, Octave.C6)
);
public static readonly E8: Position = new Position(
'E8',
new Fret(GuitarString.Sixth, 21, Pitch.DFlat, Octave.C4),
new Fret(GuitarString.First, 24, Pitch.E, Octave.C6)
);
contains(fret: Fret, lowerMargin = 0, higherMargin = 0): boolean {
return fret.isWithin(this.lowFret, this.highFret, lowerMargin, higherMargin);
}
}
export class PositionFrets {
private readonly frets: Fret[][] = [];
constructor(position: Position, guitarStrings: GuitarStrings) {
for (const guitarString of guitarStrings) {
this.frets.push([...guitarString.fretsFromTo(position.Low, position.High)]);
}
}
horizontalFretsFor(guitarString: GuitarString) {
for (const fretsOnString of this.frets) {
if (fretsOnString.some((f) => f.String == guitarString)) {
return new HorizontalFrets(fretsOnString);
}
}
return new HorizontalFrets();
}
verticalFretsAt(fretNumber: number) {
const vFrets: Fret[] = [];
for (const fretsOnString of this.frets) {
const fret = fretsOnString.find((f) => f.Number == fretNumber);
if (fret) {
vFrets.push(fret);
}
}
return new VerticalFrets(vFrets);
}
// eslint-disable-next-line sonarjs/cognitive-complexity
mapMelodicLine(line: MelodicLine): HorizontalFrets {
const fretsForLine: HorizontalFrets = new HorizontalFrets();
for (const note of line) {
const frets = this.frets.flatMap((fs) =>
fs.filter((f) => note.hasSamePitch(f.Pitch!) && note.hasSameOctave(f.Octave!))
);
if (frets && frets.length === 1) {
fretsForLine.push(frets[0]!);
continue;
}
if (frets && frets.length > 1) {
if (!fretsForLine.last()) {
fretsForLine.push(frets[0]!);
continue;
}
for (const f of frets) {
if (f.isOnSameStringAs(fretsForLine.last()!)) {
fretsForLine.push(f);
break;
}
}
}
}
return fretsForLine;
}
}
export class GuitarChord implements Iterable<Fret> {
private chordFrets: VerticalFrets = new VerticalFrets();
private position: Position = Position.Open;
toTab(): TabColumn {
return TabColumn.fromFrets([...this.chordFrets].reverse());
}
toString(): string {
return this.chordFrets.toString();
}
*[Symbol.iterator](): Iterator<Fret> {
for (const fret of this.chordFrets) {
yield fret;
}
}
public static inPosition(
chord: Chord,
position: Position,
guitarStrings: GuitarStrings = new GuitarStrings()
): GuitarChord {
const guitarChord = new GuitarChord();
guitarChord.position = position;
if (position === Position.Open) {
guitarChord.chordFrets = new VerticalFrets(
guitarChord.mapOpenPositionChord(chord, guitarStrings)
);
return guitarChord;
}
guitarChord.chordFrets = new VerticalFrets(
guitarChord.mapNonOpenePositionChord(chord, guitarStrings)
);
return guitarChord;
}
public static fromBassString(chord: Chord, bass: GuitarString): GuitarChord {
const guitarChord = new GuitarChord();
guitarChord.mapFromBassString(chord, bass);
guitarChord.chordFrets.adjustOpenStringOctaves();
guitarChord.chordFrets.reverse();
return guitarChord;
}
private mapFromBassString(chord: Chord, guitarString: GuitarString) {
for (const pitch of chord) {
let fret = guitarString.fretFor(pitch);
if (this.chordFrets.isTooFar(fret)) {
fret = fret.raiseOctave();
}
if (this.chordFrets.isTooFar(fret)) {
guitarString = guitarString.NextAscending;
fret = guitarString.fretFor(pitch);
}
this.addFretFor(fret, guitarString);
guitarString = guitarString.NextAscending;
}
}
private addFretFor(fret: Fret, guitarString: GuitarString) {
this.chordFrets.updateFretAt(fret, guitarString.Index - 1);
}
private mapOpenPositionChord(chord: Chord, guitarStrings: GuitarStrings): Fret[] {
const mappedeFrets: Fret[] = [];
for (const guitarString of guitarStrings) {
for (const pitch of chord) {
const fret = guitarString.fretFor(pitch);
if (this.position.contains(fret, 1, 1)) {
mappedeFrets.push(fret);
break;
}
}
}
return mappedeFrets;
}
private mapNonOpenePositionChord(chord: Chord, guitarStrings: GuitarStrings): Fret[] {
const mappedeFrets: Fret[] = [];
for (const guitarString of guitarStrings) {
for (const pitch of chord) {
if (mappedeFrets.find((f) => f.Pitch === pitch)) {
continue;
}
const fret = guitarString.fretFor(pitch);
if (this.position.contains(fret, 1, 1)) {
mappedeFrets.push(fret);
}
}
if (!mappedeFrets.find((f) => f.String === guitarString)) {
mappedeFrets.push(new BlankFret());
}
}
return mappedeFrets;
}
}
export class GuitarPitchLine {
protected readonly line: HorizontalFrets = new HorizontalFrets();
private readonly position: Position = Position.Open;
private readonly guitarStrings = new GuitarStrings();
constructor(
pitchLine: PitchLine,
position: Position,
guitarStrings: GuitarStrings = new GuitarStrings()
) {
this.position = position;
this.guitarStrings = guitarStrings;
this.line =
pitchLine.length == 0 ? new HorizontalFrets() : this.mapPitchLine(pitchLine, guitarStrings);
}
toTab(): Tab {
return this.line.toTab(this.guitarStrings);
}
protected mapPitchLine(pitchLine: PitchLine, guitarStrings: GuitarStrings) {
return this.mapLine(pitchLine, this.guitarStringsFor(pitchLine.Direction, guitarStrings));
}
private guitarStringsFor(lineDirection: PitchLineDirection, guitarStrings: GuitarStrings) {
const lastFret = this.line.last();
if (lastFret) {
return guitarStrings.sortByDirectionAndString(lineDirection, lastFret.String);
}
return guitarStrings;
}
private mapLine(pitchLine: PitchLine, guitarStrings: GuitarStrings) {
const line = new HorizontalFrets();
for (const pitch of pitchLine) {
for (const guitarString of guitarStrings) {
const lastFret = line.last();
if (lastFret && this.skipString(lastFret, guitarString, pitchLine.Direction)) {
continue;
}
if (
pitchLine.Direction == PitchLineDirection.OctaveDown &&
this.mapPitch(pitch, guitarString.NextDescending.NextDescending, line)
) {
break;
}
if (this.mapPitch(pitch, guitarString, line)) {
break;
}
}
}
return line;
}
private skipString(
lastFretOnLine: Fret,
guitarString: GuitarString,
direction: PitchLineDirection
) {
return direction == PitchLineDirection.Ascending
? guitarString.isHigherIndexThan(lastFretOnLine.String)
: guitarString.isLowerIndexThan(lastFretOnLine.String);
}
private mapPitch(pitch: Pitch, guitarString: GuitarString, line: HorizontalFrets): boolean {
const fret = guitarString.fretFor(pitch);
if (this.position.contains(fret)) {
line.push(fret);
return true;
}
return false;
}
}
export class GuitarPitchLines extends GuitarPitchLine {
constructor(
pitchLines: PitchLines,
position: Position,
guitarStrings: GuitarStrings = new GuitarStrings()
) {
super(new PitchLine(), position, guitarStrings);
for (const pitchLine of pitchLines) {
if (pitchLine.length > 0) {
this.line.concat(this.mapPitchLine(pitchLine, guitarStrings));
}
}
}
}
export class GuitarHarmonicLine {
private readonly chords: GuitarChord[] = [];
private readonly bassString: GuitarString = GuitarString.Sixth;
constructor(bassString: GuitarString, chords: Chord[] = []) {
this.bassString = bassString;
chords.forEach((c) => this.add(c));
}
add(chord: Chord) {
this.chords.push(GuitarChord.fromBassString(chord, this.bassString));
}
toTab() {
const column = [...this.chords].map((c) => c.toTab());
return new Tab(...column);
}
}
export class Tab {
private tab: string[][];
constructor(...columns: TabColumn[]) {
this.tab = this.renderColumns(columns);
}
sufixWith(...columns: TabColumn[]) {
this.tab = this.tab.concat(this.renderColumns(columns));
return this;
}
prefixWith(...columns: TabColumn[]) {
this.tab = this.renderColumns(columns).concat(this.tab);
return this;
}
separateWith(separator: string): this {
this.tab = this.tab.map((column, i) =>
i == this.tab.length - 1 ? column : column.map((value) => `${value}${separator}`)
);
return this;
}
render(): string[][] {
return this.tab;
}
static render(tab: Tab = new Tab()): string {
return tab
.separateWith('-')
.prefixWith(TabColumn.Start)
.prefixWith(TabColumn.StandardTunning)
.sufixWith(TabColumn.End)
.render()
.reduce((acc, column) => acc.map((row, i) => row.concat(column[i] ?? ''), []))
.join('\n');
}
static renderColumn(column: TabColumn): string {
return Tab.render(new Tab(column));
}
private renderColumns(columns: TabColumn[]): string[][] {
return columns.map((column) => column.render());
}
}