Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/alphatab/src/exporter/Gp7Exporter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Logger } from '@coderline/alphatab/Logger';
import { JsonConverter } from '@coderline/alphatab/model/JsonConverter';
import type { Score } from '@coderline/alphatab/model/Score';
import type { Settings } from '@coderline/alphatab/Settings';
import { ZipEntry } from '@coderline/alphatab/zip/ZipEntry';
import { ScoreExporter } from '@coderline/alphatab/exporter/ScoreExporter';
import { GpifWriter } from '@coderline/alphatab/exporter//GpifWriter';
Expand All @@ -18,6 +20,10 @@ export class Gp7Exporter extends ScoreExporter {
}

public writeScore(score: Score): void {
// GP7+ requires string and fret information for all notes which we might need to assign
// during export. We work on a copy to keep the input score untouched.
score = Gp7Exporter._cloneScore(score, this.settings);

Logger.debug(this.name, 'Writing data entries');
const gpifWriter: GpifWriter = new GpifWriter();
const gpifXml = gpifWriter.writeXml(score);
Expand All @@ -40,4 +46,13 @@ export class Gp7Exporter extends ScoreExporter {

fileSystem.end();
}

private static _cloneScore(score: Score, settings: Settings): Score {
const clone = JsonConverter.jsObjectToScore(JsonConverter.scoreToJsObject(score), settings);
// the raw audio is not serialized, we only read it during export so we can share it
if (score.backingTrack && clone.backingTrack) {
clone.backingTrack.rawAudioFile = score.backingTrack.rawAudioFile;
}
return clone;
}
}
4 changes: 2 additions & 2 deletions packages/alphatab/src/exporter/GpifWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ export class GpifWriter {
const stringedTuning = needsFingering ? this._tuningByStaff.get(staff)! : null;
// Once the assigner sets note.string/note.fret, note.realValue
// routes through staff.tuning — needs to match the tuning we
// gave the assigner. Save + restore below leaves the input
// model untouched.
// gave the assigner. The notes are modified, hence the
// Gp7Exporter passes a copy of the input score.
const savedTunings = staff.tuning;
if (needsFingering && stringedTuning !== null && savedTunings.length === 0) {
staff.stringTuning.tunings = stringedTuning.slice();
Expand Down
40 changes: 38 additions & 2 deletions packages/alphatab/src/importer/MusicXmlImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2463,6 +2463,13 @@ export class MusicXmlImporter extends ScoreImporter {
newVoiceNumber = 0;
}

// the first voice on the staff takes the initial voice which every bar is created with
if (packing.sortedRawVoices.length === 0) {
packing.sortedRawVoices.push(rawVoice);
packing.mapping.set(rawVoice, 0);
return bar.voices[0];
}

// find sorted-insertion position
let insertPos = packing.sortedRawVoices.length;
for (let i = 0; i < packing.sortedRawVoices.length; i++) {
Expand Down Expand Up @@ -2837,6 +2844,29 @@ export class MusicXmlImporter extends ScoreImporter {
// beat/voice/bar/staff tree (e.g. percussion clef context on the resolved staff).
// Therefore this must run after ensureBeat() and after transposition has been applied.
this._finalizeImportedNote(note, track, instrumentId, isPitched, noteIsVisible);
this._finalizeStringNumber(note);
}
}

/**
* Validates the string parsed from `<technical><string>` and decides whether it is a
* tab position (string + fret) or a string number annotation on a pitched note.
*/
private _finalizeStringNumber(note: Note) {
if (Number.isNaN(note.string)) {
return;
}

const stringCount = Note.getStringCount(note.beat.voice.bar.staff);
if (note.string < 1 || note.string > stringCount) {
Logger.warning('MusicXML', `Ignoring <string> outside of the available ${stringCount} strings`);
note.string = Number.NaN;
note.fret = Number.NaN;
return;
}

if (!note.isStringed && !note.isPercussion) {
note.showStringNumber = true;
}
}

Expand Down Expand Up @@ -3601,12 +3631,18 @@ export class MusicXmlImporter extends ScoreImporter {
// case 'snap-pizzicato': Not supported
case 'fret':
if (note) {
note.fret = Number.parseInt(c.innerText, 10);
if (beat.voice.bar.staff.tuning.length > 0) {
note.fret = Number.parseInt(c.innerText, 10);
} else {
// without tuning the fret has no meaning, the pitch defines the note
Logger.warning('MusicXML', 'Ignoring <fret> on staff without tuning');
}
}
break;
case 'string':
if (note) {
note.string = beat.voice.bar.staff.tuning.length - Number.parseInt(c.innerText, 10) + 1;
// on staves without tuning the string is only an annotation (e.g. classical guitar string indications)
note.string = Note.getStringCount(beat.voice.bar.staff) - Number.parseInt(c.innerText, 10) + 1;
}
break;
case 'hammer-on':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,7 @@ export class AlphaTex1LanguageDefinitions {
['iturn', null],
['umordent', null],
['lmordent', null],
['string', null],
['string', [[], [[[106], 0]]]],
['hide', null],
[
'b',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType';
import { Lyrics } from '@coderline/alphatab/model/Lyrics';
import { BeamingRules, type MasterBar } from '@coderline/alphatab/model/MasterBar';
import { ModelUtils } from '@coderline/alphatab/model/ModelUtils';
import type { Note } from '@coderline/alphatab/model/Note';
import { Note } from '@coderline/alphatab/model/Note';
import { NoteAccidentalMode } from '@coderline/alphatab/model/NoteAccidentalMode';
import { NoteOrnament } from '@coderline/alphatab/model/NoteOrnament';
import { Ottavia } from '@coderline/alphatab/model/Ottavia';
Expand Down Expand Up @@ -2368,6 +2368,33 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler
note.ornament = NoteOrnament.LowerMordent;
return ApplyNodeResult.Applied;
case 'string':
if (p.arguments && p.arguments.arguments.length > 0) {
const stringArg = p.arguments.arguments[0] as AlphaTexNumberLiteral;
if (!note.isPiano) {
importer.addSemanticDiagnostic({
code: AlphaTexDiagnosticCode.AT221,
message: `A string number can only be specified on pitched notes, use the 'fret.string' syntax to specify the string of fretted notes.`,
severity: AlphaTexDiagnosticsSeverity.Error,
start: stringArg.start,
end: stringArg.end
});
return ApplyNodeResult.NotAppliedSemanticError;
}

const stringCount = Note.getStringCount(note.beat.voice.bar.staff);
const stringNumber = stringArg.value;
if (stringNumber < 1 || stringNumber > stringCount) {
importer.addSemanticDiagnostic({
code: AlphaTexDiagnosticCode.AT211,
message: `Value is out of valid range. Allowed range: 1-${stringCount}, Actual Value: ${stringNumber}`,
severity: AlphaTexDiagnosticsSeverity.Error,
start: stringArg.start,
end: stringArg.end
});
return ApplyNodeResult.NotAppliedSemanticError;
}
note.string = stringCount - stringNumber + 1;
}
note.showStringNumber = true;
return ApplyNodeResult.Applied;
case 'hide':
Expand Down Expand Up @@ -3291,7 +3318,12 @@ export class AlphaTex1LanguageHandler implements IAlphaTexLanguageImportHandler
}

if (note.showStringNumber) {
Atnf.prop(properties, 'string');
if (note.isPiano && !Number.isNaN(note.string)) {
const stringNumber = Note.getStringCount(note.beat.voice.bar.staff) - note.string + 1;
Atnf.prop(properties, 'string', Atnf.numberValue(stringNumber));
} else {
Atnf.prop(properties, 'string');
}
}

if (note.isTrill) {
Expand Down
5 changes: 5 additions & 0 deletions packages/alphatab/src/importer/alphaTex/AlphaTexShared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,11 @@ export enum AlphaTexDiagnosticCode {
*/
AT220 = 220,

/**
* A string number can only be specified on pitched notes, use the 'fret.string' syntax to specify the string of fretted notes.
*/
AT221 = 221,

/**
* Expected no arguments, but found some.
*/
Expand Down
17 changes: 17 additions & 0 deletions packages/alphatab/src/model/Note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,14 @@ export class Note {
* Gets or sets the string number where the note is placed.
* 1 is the lowest string on the guitar and the bottom line on the tablature.
* It then increases the the number of strings on available on the track.
* On pitched notes (no fret) the string is only an annotation shown via {@link showStringNumber}
* and has no effect on playback. Staves without tuning assume a standard 6 string instrument for this case.
*/
public string: number = Number.NaN;

/**
* Gets or sets whether the string number for this note should be shown.
* For pitched notes this requires {@link string} to be set.
*/
public showStringNumber: boolean = false;

Expand Down Expand Up @@ -639,6 +642,20 @@ export class Note {
return 0;
}

/**
* The number of strings assumed for string number annotations on staves without tuning.
*/
private static readonly _defaultAnnotationStringCount: number = 6;

/**
* Gets the number of strings to which {@link string} relates on the given staff.
* Staves without tuning assume a standard 6 string instrument (used for string number annotations on pitched notes).
* @internal
*/
public static getStringCount(staff: Staff): number {
return staff.tuning.length > 0 ? staff.tuning.length : Note._defaultAnnotationStringCount;
}

public get realValue(): number {
return this.calculateRealValue(true, true);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/alphatab/src/rendering/glyphs/ScoreBeatGlyph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ export class ScoreBeatGlyph extends BeatOnNoteGlyphBase {
if (n.accentuated === AccentuationType.Tenuto && !belowBeatEffects.has('Tenuto')) {
outsideBeatEffects.set('Tenuto', new AccentuationGlyph(0, 0, n));
}
if (n.showStringNumber && n.isStringed) {
if (n.showStringNumber && !Number.isNaN(n.string)) {
let container: StringNumberContainerGlyph;
if (!aboveBeatEffects.has('StringNumber')) {
container = new StringNumberContainerGlyph(0, 0);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph';
import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol';
import { Note } from '@coderline/alphatab/model/Note';
import { CanvasHelper, type ICanvas } from '@coderline/alphatab/platform/ICanvas';

/**
Expand All @@ -20,13 +21,13 @@ export class StringNumberContainerGlyph extends EffectGlyph {
}

public override paint(cx: number, cy: number, canvas: ICanvas): void {
const tuningLength = this.renderer.bar.staff.tuning.length;
const stringCount = Note.getStringCount(this.renderer.bar.staff);

let y = 0;
const circleHeight =
this.renderer.smuflMetrics.glyphWidths.get(MusicFontSymbol.GuitarString0)! * this.renderer.smuflMetrics.tuningGlyphCircleNumberScale;
for (const s of this._strings) {
const stringValue = tuningLength - s;
const stringValue = stringCount - s;
const symbol = ((MusicFontSymbol.GuitarString1 as number) + stringValue) as MusicFontSymbol;
CanvasHelper.fillMusicFontSymbolSafe(canvas,
cx + this.x,
Expand Down
Binary file modified packages/alphatab/test-data/musicxml-testsuite/32a-Notations.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading