Skip to content

Commit da088e0

Browse files
authored
fix: handle harmonic values in gp3-5 at best knowledge (#2707)
1 parent c3bb5dc commit da088e0

7 files changed

Lines changed: 120 additions & 20 deletions

File tree

packages/alphatab/src/importer/Gp3To5Importer.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -901,9 +901,7 @@ export class Gp3To5Importer extends ScoreImporter {
901901
const note = this.readNote(track, bar, voice, newBeat, 6 - i);
902902
if (allNoteHarmonicType !== HarmonicType.None) {
903903
note.harmonicType = allNoteHarmonicType;
904-
if (note.harmonicType === HarmonicType.Natural) {
905-
note.harmonicValue = ModelUtils.deltaFretToHarmonicValue(note.fret);
906-
}
904+
note.harmonicValue = ModelUtils.deltaFretToHarmonicValue(note.fret);
907905
}
908906
}
909907
}
@@ -1600,46 +1598,74 @@ export class Gp3To5Importer extends ScoreImporter {
16001598
note.harmonicValue = ModelUtils.deltaFretToHarmonicValue(note.fret);
16011599
break;
16021600
case 2:
1603-
/*let _harmonicTone: number = */ this.data.readByte();
1604-
/*let _harmonicKey: number = */ this.data.readByte();
1605-
/*let _harmonicOctaveOffset: number = */ this.data.readByte();
1601+
// C (0), D (2), E (4), F (5), G (7),A (9),B (11)
1602+
const harmonicTone: number = this.data.readByte();
1603+
// b (255/-1), none (0), # (1)
1604+
let harmonicKey: number = this.data.readByte();
1605+
if (harmonicKey === 255) {
1606+
harmonicKey = -1;
1607+
}
1608+
// Loco (0), 8va (1), 15ma (2)
1609+
const harmonicOctaveOffset: number = this.data.readByte();
1610+
1611+
const harmonicPitch = harmonicTone + harmonicKey; // 0-11 pitch class
1612+
const playedPitch = (note.fret + note.stringTuning) % 12; // 0-11 pitch class
1613+
1614+
let targetHarmonic = harmonicPitch + harmonicOctaveOffset * 12;
1615+
1616+
// Adjust to ensure harmonic is higher than played note (single octave should be enough with 0-11 played pitch)
1617+
if (targetHarmonic < playedPitch) {
1618+
targetHarmonic += 12;
1619+
}
1620+
1621+
const deltaFrets = targetHarmonic - playedPitch;
16061622
note.harmonicType = HarmonicType.Artificial;
1623+
note.harmonicValue = ModelUtils.deltaFretToHarmonicValue(deltaFrets);
1624+
16071625
break;
16081626
case 3:
16091627
note.harmonicType = HarmonicType.Tap;
16101628
note.harmonicValue = ModelUtils.deltaFretToHarmonicValue(this.data.readByte());
16111629
break;
16121630
case 4:
16131631
note.harmonicType = HarmonicType.Pinch;
1614-
note.harmonicValue = 12;
1632+
note.harmonicValue = ModelUtils.deltaFretToHarmonicValue(12);
16151633
break;
16161634
case 5:
16171635
note.harmonicType = HarmonicType.Semi;
1618-
note.harmonicValue = 12;
1636+
note.harmonicValue = ModelUtils.deltaFretToHarmonicValue(12);
16191637
break;
16201638
}
16211639
} else if (this._versionNumber >= 400) {
16221640
switch (type) {
16231641
case 1:
16241642
note.harmonicType = HarmonicType.Natural;
1643+
note.harmonicValue = ModelUtils.deltaFretToHarmonicValue(note.fret);
16251644
break;
16261645
case 3:
16271646
note.harmonicType = HarmonicType.Tap;
1647+
// GP4 help: The tapped harmonic is an artificial harmonic obtained by tapping quickly on the string 12 frets higher.
1648+
note.harmonicValue = ModelUtils.deltaFretToHarmonicValue(12);
16281649
break;
16291650
case 4:
16301651
note.harmonicType = HarmonicType.Pinch;
1652+
note.harmonicValue = ModelUtils.deltaFretToHarmonicValue(12);
16311653
break;
16321654
case 5:
16331655
note.harmonicType = HarmonicType.Semi;
1656+
note.harmonicValue = ModelUtils.deltaFretToHarmonicValue(12);
16341657
break;
1635-
case 15:
1658+
case 15: // artificial + 5
16361659
note.harmonicType = HarmonicType.Artificial;
1660+
note.harmonicValue = ModelUtils.deltaFretToHarmonicValue(5);
16371661
break;
1638-
case 17:
1662+
case 17: // artificial + 7
16391663
note.harmonicType = HarmonicType.Artificial;
1664+
note.harmonicValue = ModelUtils.deltaFretToHarmonicValue(7);
16401665
break;
1641-
case 22:
1666+
case 22: // artificial + 12
16421667
note.harmonicType = HarmonicType.Artificial;
1668+
note.harmonicValue = ModelUtils.deltaFretToHarmonicValue(12);
16431669
break;
16441670
}
16451671
}
989 Bytes
Binary file not shown.
1.09 KB
Binary file not shown.
1.58 KB
Binary file not shown.

packages/alphatab/test/importer/Gp3Importer.test.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { describe, expect, it } from 'vitest';
21
import { AutomationType } from '@coderline/alphatab/model/Automation';
32
import { BrushType } from '@coderline/alphatab/model/BrushType';
43
import { DynamicValue } from '@coderline/alphatab/model/DynamicValue';
4+
import { HarmonicType } from '@coderline/alphatab/model/HarmonicType';
55
import type { Score } from '@coderline/alphatab/model/Score';
66
import { SlideOutType } from '@coderline/alphatab/model/SlideOutType';
77
import { GpImporterTestHelper } from 'test/importer/GpImporterTestHelper';
8-
import { HarmonicType } from '@coderline/alphatab/model/HarmonicType';
8+
import { describe, expect, it } from 'vitest';
99
describe('Gp3ImporterTest', () => {
1010
it('score-info', async () => {
1111
const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/score-info.gp3');
@@ -155,4 +155,20 @@ describe('Gp3ImporterTest', () => {
155155
const score: Score = reader.readScore();
156156
GpImporterTestHelper.checkStrings(score);
157157
});
158+
159+
it('beat-harmonics', async () => {
160+
const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro3/beat-harmonics.gp3');
161+
const score = reader.readScore();
162+
const b0 = score.tracks[0].staves[0].bars[0].voices[0].beats[0];
163+
const b1 = score.tracks[0].staves[0].bars[0].voices[0].beats[1];
164+
expect(b0.notes[0].harmonicType).toBe(HarmonicType.Natural);
165+
expect(b0.notes[0].harmonicValue).toBe(12);
166+
expect(b0.notes[1].harmonicType).toBe(HarmonicType.Natural);
167+
expect(b0.notes[1].harmonicValue).toBe(12);
168+
expect(b1.notes[0].harmonicType).toBe(HarmonicType.Artificial);
169+
expect(b1.notes[0].harmonicValue).toBe(12);
170+
expect(b1.notes[1].harmonicType).toBe(HarmonicType.Artificial);
171+
expect(b1.notes[1].harmonicValue).toBe(12);
172+
});
158173
});
174+

packages/alphatab/test/importer/Gp4Importer.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { describe, expect, it } from 'vitest';
1+
import { HarmonicType } from '@coderline/alphatab/model/HarmonicType';
22
import type { Score } from '@coderline/alphatab/model/Score';
33
import { GpImporterTestHelper } from 'test/importer/GpImporterTestHelper';
4+
import { describe, expect, it } from 'vitest';
45
describe('Gp4ImporterTest', () => {
56
it('score-info', async () => {
67
const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/score-info.gp4');
@@ -140,4 +141,36 @@ describe('Gp4ImporterTest', () => {
140141
const score: Score = reader.readScore();
141142
GpImporterTestHelper.checkColors(score);
142143
});
144+
145+
it('harmonic-types', async () => {
146+
const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro4/harmonic-types.gp4');
147+
const score = reader.readScore();
148+
const b0 = score.tracks[0].staves[0].bars[0].voices[0].beats[0];
149+
expect(b0.notes[0].harmonicType).toBe(HarmonicType.Natural);
150+
expect(b0.notes[0].harmonicValue).toBe(12);
151+
152+
const b1 = score.tracks[0].staves[0].bars[0].voices[0].beats[1];
153+
expect(b1.notes[0].harmonicType).toBe(HarmonicType.Artificial);
154+
expect(b1.notes[0].harmonicValue).toBe(5);
155+
156+
const b2 = score.tracks[0].staves[0].bars[0].voices[0].beats[2];
157+
expect(b2.notes[0].harmonicType).toBe(HarmonicType.Artificial);
158+
expect(b2.notes[0].harmonicValue).toBe(7);
159+
160+
const b3 = score.tracks[0].staves[0].bars[0].voices[0].beats[3];
161+
expect(b3.notes[0].harmonicType).toBe(HarmonicType.Artificial);
162+
expect(b3.notes[0].harmonicValue).toBe(12);
163+
164+
const b4 = score.tracks[0].staves[0].bars[1].voices[0].beats[0];
165+
expect(b4.notes[0].harmonicType).toBe(HarmonicType.Tap);
166+
expect(b4.notes[0].harmonicValue).toBe(12);
167+
168+
const b5 = score.tracks[0].staves[0].bars[1].voices[0].beats[1];
169+
expect(b5.notes[0].harmonicType).toBe(HarmonicType.Pinch);
170+
expect(b5.notes[0].harmonicValue).toBe(12);
171+
172+
const b6 = score.tracks[0].staves[0].bars[1].voices[0].beats[2];
173+
expect(b6.notes[0].harmonicType).toBe(HarmonicType.Semi);
174+
expect(b6.notes[0].harmonicValue).toBe(12);
175+
});
143176
});

packages/alphatab/test/importer/Gp5Importer.test.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
import { describe, expect, it } from 'vitest';
21
import { Settings } from '@coderline/alphatab/Settings';
32
import { GpBinaryHelpers } from '@coderline/alphatab/importer/Gp3To5Importer';
43
import { ByteBuffer } from '@coderline/alphatab/io/ByteBuffer';
4+
import { EndOfReaderError, OverflowError } from '@coderline/alphatab/io/IReadable';
55
import { type Beat, BeatBeamingMode } from '@coderline/alphatab/model/Beat';
6+
import { Clef } from '@coderline/alphatab/model/Clef';
67
import { Direction } from '@coderline/alphatab/model/Direction';
8+
import { HarmonicType } from '@coderline/alphatab/model/HarmonicType';
79
import { Ottavia } from '@coderline/alphatab/model/Ottavia';
10+
import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper';
811
import { type Score, ScoreSubElement } from '@coderline/alphatab/model/Score';
912
import { WahPedal } from '@coderline/alphatab/model/WahPedal';
1013
import { TextAlign } from '@coderline/alphatab/platform/ICanvas';
1114
import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection';
12-
import { GpImporterTestHelper } from 'test/importer/GpImporterTestHelper';
13-
import { Clef } from '@coderline/alphatab/model/Clef';
14-
import { PercussionMapper } from '@coderline/alphatab/model/PercussionMapper';
15-
import { EndOfReaderError, OverflowError } from '@coderline/alphatab/io/IReadable';
1615
import { TestPlatform } from 'test/TestPlatform';
16+
import { GpImporterTestHelper } from 'test/importer/GpImporterTestHelper';
17+
import { describe, expect, it } from 'vitest';
1718

1819
describe('Gp5ImporterTest', () => {
1920
it('score-info', async () => {
@@ -636,7 +637,31 @@ describe('Gp5ImporterTest', () => {
636637

637638
const importer = GpImporterTestHelper.prepareImporterWithBytes(buffer, new Settings());
638639

639-
expect(()=> importer.readScore()).toThrow(EndOfReaderError);
640+
expect(() => importer.readScore()).toThrow(EndOfReaderError);
640641
});
641642
});
643+
644+
it('harmonic-types', async () => {
645+
const reader = await GpImporterTestHelper.prepareImporterWithFile('guitarpro5/harmonic-types.gp5');
646+
const score = reader.readScore();
647+
const b0 = score.tracks[0].staves[0].bars[0].voices[0].beats[0];
648+
expect(b0.notes[0].harmonicType).toBe(HarmonicType.Natural);
649+
expect(b0.notes[0].harmonicValue).toBe(12);
650+
651+
const b1 = score.tracks[0].staves[0].bars[0].voices[0].beats[1];
652+
expect(b1.notes[0].harmonicType).toBe(HarmonicType.Artificial);
653+
expect(b1.notes[0].harmonicValue).toBe(17);
654+
655+
const b2 = score.tracks[0].staves[0].bars[0].voices[0].beats[2];
656+
expect(b2.notes[0].harmonicType).toBe(HarmonicType.Tap);
657+
expect(b2.notes[0].harmonicValue).toBe(12);
658+
659+
const b3 = score.tracks[0].staves[0].bars[0].voices[0].beats[3];
660+
expect(b3.notes[0].harmonicType).toBe(HarmonicType.Pinch);
661+
expect(b3.notes[0].harmonicValue).toBe(12);
662+
663+
const b4 = score.tracks[0].staves[0].bars[1].voices[0].beats[0];
664+
expect(b4.notes[0].harmonicType).toBe(HarmonicType.Semi);
665+
expect(b4.notes[0].harmonicValue).toBe(12);
666+
});
642667
});

0 commit comments

Comments
 (0)