Skip to content

MusicXML: a LetRing <words> + <dashes> span is drawn but produces no audible let ring #2867

Description

@ppisljar

Is there an existing issue for this?

  • I have searched the existing issues

I have read the contribution rules

AI authorship

AI-assisted (disclosure block included as described in AGENTS.md)

Current Behavior

Note

AI-authored disclosure (alphatab-ai-authored-v1)

Portions of this content were authored by an AI agent. The agent has read
AGENTS.md and the human submitter accepts responsibility for
compliance with the rules in that document.

A let-ring passage imported from MusicXML — a <words>LetRing</words> direction followed by <dashes> — is drawn correctly but is not audible.

The notation is right: the "Let Ring" label and its dashed extension appear over the passage, exactly as expected. But on playback every note still stops at its own written length, so nothing rings on. An arpeggio that should pile up into a chord instead comes out as four separate, disconnected notes.

The same passage written in alphaTex with {lr} sounds correct — each note sustains to the end of the span — so the notation and the audio disagree only on the MusicXML side.

Taking four quarter notes across one 4/4 bar, all inside one let-ring span, and reading the note lengths out of the generated MIDI:

source notation shows "let ring" MIDI note lengths (ticks)
MusicXML, <words>LetRing</words> + <dashes> yes 960, 960, 960, 960
alphaTex {lr} on each note yes 3840, 2880, 1920, 960

The alphaTex row is what let ring should sound like: the first note runs to the end of the bar, the second to the end of the bar, and so on. The MusicXML row is just four ordinary quarter notes.

Expected Behavior

A <words>LetRing</words> + <dashes> span should make the notes it covers ring on in playback, the way alphaTex {lr} and Guitar Pro let-ring already do — the notes should sustain past their written length until the span ends, not just be labelled as let-ring in the notation.

The MusicXML support matrix lists directiondashes (if preceeded by words LetRing) as ✅ Supported for Model, Reading, Render, Audio and Tex. Reading and Render do work; Audio is the part that does not, which is why I am reporting this as a bug rather than asking for a new feature.

Steps To Reproduce

  1. Load the MusicXML below — four quarter notes on open strings in one 4/4 bar, wrapped in a <words>LetRing</words> + <dashes type="start"><dashes type="stop"> span.
  2. Generate the MIDI (or press play in a browser) and look at how long each note lasts.
  3. Observe that the "Let Ring" marking is drawn over the bar, but every note ends after its own quarter-note length instead of ringing on.
import * as alphaTab from '@coderline/alphatab';

const staffTuning = `<staff-details><staff-lines>6</staff-lines>
  <staff-tuning line="1"><tuning-step>E</tuning-step><tuning-octave>2</tuning-octave></staff-tuning>
  <staff-tuning line="2"><tuning-step>A</tuning-step><tuning-octave>2</tuning-octave></staff-tuning>
  <staff-tuning line="3"><tuning-step>D</tuning-step><tuning-octave>3</tuning-octave></staff-tuning>
  <staff-tuning line="4"><tuning-step>G</tuning-step><tuning-octave>3</tuning-octave></staff-tuning>
  <staff-tuning line="5"><tuning-step>B</tuning-step><tuning-octave>3</tuning-octave></staff-tuning>
  <staff-tuning line="6"><tuning-step>E</tuning-step><tuning-octave>4</tuning-octave></staff-tuning>
</staff-details>`;

const note = (step, octave, string) => `<note>
  <pitch><step>${step}</step><octave>${octave}</octave></pitch>
  <duration>1</duration><voice>1</voice><type>quarter</type>
  <notations><technical><string>${string}</string><fret>0</fret></technical></notations>
</note>`;

const xml = `<?xml version="1.0" encoding="UTF-8"?>
<score-partwise version="4.0">
  <part-list><score-part id="P1"><part-name>Guitar</part-name></score-part></part-list>
  <part id="P1"><measure number="1">
    <attributes>
      <divisions>1</divisions><time><beats>4</beats><beat-type>4</beat-type></time>
      <clef><sign>TAB</sign><line>5</line></clef>
      ${staffTuning}
    </attributes>
    <direction placement="above">
      <direction-type><words>LetRing</words></direction-type>
      <direction-type><dashes number="1" type="start"/></direction-type>
    </direction>
    ${note('E', 2, 6)}
    ${note('A', 2, 5)}
    ${note('D', 3, 4)}
    ${note('G', 3, 3)}
    <direction placement="above">
      <direction-type><words>LetRing</words></direction-type>
      <direction-type><dashes number="1" type="stop"/></direction-type>
    </direction>
  </measure></part>
</score-partwise>`;

function drawsLetRing(score) {
  const settings = new alphaTab.Settings();
  settings.core.engine = 'svg';
  settings.core.enableLazyLoading = false;
  settings.display.staveProfile = alphaTab.StaveProfile.Tab;
  const renderer = new alphaTab.rendering.ScoreRenderer(settings);
  renderer.width = 900;
  let svg = '';
  renderer.partialRenderFinished.on(r => { svg += r.renderResult; });
  renderer.renderScore(score, [0]);
  return /let\s*ring/i.test(svg);
}

function noteLengths(score) {
  const midi = new alphaTab.midi.MidiFile();
  new alphaTab.midi.MidiFileGenerator(score, new alphaTab.Settings(),
    new alphaTab.midi.AlphaSynthMidiFileHandler(midi)).generate();
  const on = new Map(), out = [];
  for (const e of midi.events) {
    if (e.type === alphaTab.midi.MidiEventType.NoteOn) on.set(e.noteKey, e.tick);
    else if (e.type === alphaTab.midi.MidiEventType.NoteOff && on.has(e.noteKey)) {
      out.push(e.tick - on.get(e.noteKey));
      on.delete(e.noteKey);
    }
  }
  return out;
}

const fromXml = alphaTab.importer.ScoreLoader.loadScoreFromBytes(new TextEncoder().encode(xml));
const fromTex = alphaTab.importer.ScoreLoader.loadAlphaTex(
  '\\tuning E4 B3 G3 D3 A2 E2 . :4 0.6{lr} 0.5{lr} 0.4{lr} 0.3{lr}');

console.log('MusicXML  draws "let ring":', drawsLetRing(fromXml), '| note lengths:', noteLengths(fromXml));
console.log('alphaTex  draws "let ring":', drawsLetRing(fromTex), '| note lengths:', noteLengths(fromTex));

Output on 1.8.4:

MusicXML  draws "let ring": true | note lengths: [ 960, 960, 960, 960 ]
alphaTex  draws "let ring": true | note lengths: [ 3840, 2880, 1920, 960 ]

Link to jsFiddle, CodePen, Project

No hosted example — the snippet above is self-contained and needs no score file.

Version and Environment

[AlphaTab][VersionInfo] alphaTab 1.8.4
[AlphaTab][VersionInfo] commit: 022a45c8e42370f9e12e68949d11eada370da83d
[AlphaTab][VersionInfo] build date: 2026-07-05T14:46:18.224Z
[AlphaTab][VersionInfo] High DPI: 1
[AlphaTab][VersionInfo] Platform: NodeJs
[AlphaTab][VersionInfo] WebPack: false
[AlphaTab][VersionInfo] Vite: false

Node.js v22.20.0, macOS 15.6.1 (Darwin 24.6.0)

The above is the real printEnvironmentInfo() output from the environment where the snippet was run.

Platform

Node.js (where the reproduction above was run and verified)

Anything else?

Where this showed up in practice: I generate MusicXML from guitar tabs and read it back with alphaTab. Because the notation looks completely correct, this one is easy to miss — the score shows the let-ring marking, and only listening reveals that nothing sustains.

Worth mentioning in case it is the same area: palm mute written per-note as <play><mute>palm</mute></play> does change the sound — on a two-note test the marked note comes back 160 ticks instead of 960 — so per-note playback markings are getting through. It is the <words> + <dashes> span form that is drawn but not heard. I have not found any MusicXML spelling of let ring that produces audible sustain.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    Priority

    None yet

    Area

    None yet

    Platform

    None yet

    Work State

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions