Class: Musa::Transcriptors::FromGDV::ToMIDI::Trill

Inherits:
Musa::Transcription::FeatureTranscriptor show all
Defined in:
lib/musa-dsl/transcription/from-gdv-to-midi.rb

Overview

Trill transcriptor for MIDI playback.

Expands trill ornaments into a rapid alternation between the main note and its upper neighbor. The trill fills the entire note duration with alternating notes, with sophisticated duration management including acceleration.

Trill Options

  • .tr or .tr(true) - Standard trill starting with upper neighbor
  • .tr(:low) - Start with lower neighbor first (2 notes)
  • .tr(:low2) - Start with upper but include lower neighbor (4 notes)
  • .tr(:same) - Start with main note
  • .tr(factor) - Custom duration factor (e.g., .tr(1/8r))

Duration Algorithm

The trill uses a sophisticated multi-phase duration algorithm:

  1. Initial pattern: Based on trill options (:low, :low2, :same)
  2. Regular pattern: Two cycles at full note_duration
  3. Accelerando: Cycles at 2/3 note_duration (faster)
  4. Final notes: Distribute remaining duration

Processing

Given .tr on a note:

{ grade: 0, duration: 1r, tr: true }

Expands to alternating sequence:

[
  { grade: 1, duration: 1/16r },    # Upper (initial)
  { grade: 0, duration: 1/16r },    # Main
  { grade: 1, duration: 1/16r },    # Upper (regular)
  { grade: 0, duration: 1/16r },    # Main
  { grade: 1, duration: ~1/24r },   # Upper (accelerando)
  { grade: 0, duration: ~1/24r },   # Main
  ...
]

Process: .tr

Examples:

Standard trill

trill = Trill.new(duration_factor: 1/4r)
gdv = { grade: 0, duration: 1r, tr: true }
result = trill.transcript(gdv, base_duration: 1/4r, tick_duration: 1/96r)
# Generates alternating upper/main notes filling duration

Trill starting low

gdv = { grade: 0, duration: 1r, tr: :low }
# Starts with lower neighbor, then alternates upper/main

A numeric :tr, which scales this performer's trill

trill = Trill.new(duration_factor: 1/4r)

normal = trill.transcript({ grade: 0, duration: 1r, tr: true },
                          base_duration: 1/4r, tick_duration: 1/96r)
faster = trill.transcript({ grade: 0, duration: 1r, tr: 1/2r },
                          base_duration: 1/4r, tick_duration: 1/96r)

normal.size  # => 22
faster.size  # => 46

# And `tr: 1` is the neutral element: this performer's own trill.
trill.transcript({ grade: 0, duration: 1r, tr: 1r },
                 base_duration: 1/4r, tick_duration: 1/96r).size  # => 22

The same number over a faster performer gives a faster trill

quick = Trill.new(duration_factor: 1/8r)

quick.transcript({ grade: 0, duration: 1r, tr: 1/2r },
                 base_duration: 1/4r, tick_duration: 1/96r).size  # => 94

# 46 against 94 for the same `tr(1/2)`: the score says "twice as fast
# as usual" and main.rb says what usual is.

Instance Method Summary collapse

Constructor Details

#initialize(duration_factor: nil, logger: nil) ⇒ Trill

Creates trill transcriptor.

Parameters:

  • duration_factor (Rational) (defaults to: nil)

    how fast this performer trills: the trill note duration as a factor of base_duration (default: 1/4). A numeric :tr in the score is a factor over THIS, so one knob here moves every trill of the piece together.

  • logger (Musa::Logger::Logger, nil) (defaults to: nil)

    where to say that a trill was asked to go finer than the tick.



471
472
473
474
# File 'lib/musa-dsl/transcription/from-gdv-to-midi.rb', line 471

def initialize(duration_factor: nil, logger: nil)
  @duration_factor = duration_factor || 1/4r
  @logger = logger || Musa::Logger::Logger.new
end

Instance Method Details

#transcript(gdv, base_duration:, tick_duration:) ⇒ Array<Hash>, Hash

Transcribes trill to alternating note sequence.

Parameters:

  • gdv (Hash)

    GDV event possibly containing :tr

  • base_duration (Rational)

    base duration unit

  • tick_duration (Rational)

    minimum tick duration

Returns:

  • (Array<Hash>, Hash)

    array with trill notes, or unchanged event



485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
# File 'lib/musa-dsl/transcription/from-gdv-to-midi.rb', line 485

def transcript(gdv, base_duration:, tick_duration:)
  tr = gdv.delete :tr

  if tr
    note_duration = base_duration * @duration_factor

    check(tr) do |tr|
      case tr
      when Numeric
        # A factor over this transcriptor's own trill speed, not an
        # absolute duration: `tr(1/2)` is twice as fast as this
        # performer usually trills, whatever that is. So one knob in
        # main.rb governs every ornament of the piece and they all move
        # together when it changes.
        #
        # Speed is a realisation matter and not a notation one, which is
        # what the MusicXML side shows: it has no trill transcriptor at
        # all, reads :tr as "is there a trill" and writes <trill-mark/>,
        # discarding the value. A number meaning an absolute duration
        # would be an assertion the engraved score silently loses.
        #
        # It used to be `*= base_duration * tr`, applying the base
        # duration a second time and keeping the default factor the
        # caller was replacing: `tr(1/8)` gave 766 notes of 1/512 in the
        # space of a quarter (issue #83).
        note_duration *= tr.to_r
      end
    end

    # As Mordent already does with the same parameter, three hundred
    # lines up: a trill finer than the grid is not a fast trill, it is
    # an unplayable one, and several of its notes would land on the same
    # tick. Asking for one is a legitimate way to say "as fast as you
    # can"; it just should not pass unnoticed.
    #
    # The floor is a tick and a HALF, not a tick, because the middle of
    # a trill accelerates to 2/3 of its note duration -- measured, that
    # is the smallest thing it ever emits. Clamping to one tick would
    # have made the promise nominal and let the fast section through
    # below the grid anyway.
    minimum_duration = tick_duration * 3/2r

    if note_duration < minimum_duration
      # Once per transcriptor: a piece full of trills should say this
      # once, not once per note.
      unless @warned_about_tick
        @warned_about_tick = true

        @logger.warn('Trill') do
          "a trill note of #{note_duration} would go below the tick #{tick_duration} " \
            "when it accelerates; trilling at #{minimum_duration} instead"
        end
      end

      note_duration = minimum_duration
    end

    used_duration = 0r
    last = nil

    gdvs = []

    check(tr) do |tr|
      case tr
      when :low # start with lower note
        gdvs << gdv.clone.tap { |gdv| gdv[:grade] += (last = -1); gdv[:duration] = note_duration }
        gdvs << gdv.clone.tap { |gdv| gdv[:grade] += (last = 0); gdv[:duration] = note_duration }
        used_duration += 2 * note_duration

      when :low2 # start with upper note but go to lower note once
        gdvs << gdv.clone.tap { |gdv| gdv[:grade] += (last = 1); gdv[:duration] = note_duration }
        gdvs << gdv.clone.tap { |gdv| gdv[:grade] += (last = 0); gdv[:duration] = note_duration }
        gdvs << gdv.clone.tap { |gdv| gdv[:grade] += (last = -1); gdv[:duration] = note_duration }
        gdvs << gdv.clone.tap { |gdv| gdv[:grade] += (last = 0); gdv[:duration] = note_duration }
        used_duration += 4 * note_duration

      when :same # start with the same note
        gdvs << gdv.clone.tap { |gdv| gdv[:grade] += (last = 0); gdv[:duration] = note_duration }
        used_duration += note_duration
      end
    end

    2.times do
      if used_duration + 2 * note_duration <= gdv[:duration]
        gdvs << gdv.clone.tap { |gdv| gdv[:grade] += (last = 1); gdv[:duration] = note_duration }
        gdvs << gdv.clone.tap { |gdv| gdv[:grade] += (last = 0); gdv[:duration] = note_duration }

        used_duration += 2 * note_duration
      end
    end

    while used_duration + 2 * note_duration * 2/3r <= gdv[:duration]
      gdvs << gdv.clone.tap { |gdv| gdv[:grade] += (last = 1); gdv[:duration] = note_duration * 2/3r }
      gdvs << gdv.clone.tap { |gdv| gdv[:grade] += (last = 0); gdv[:duration] = note_duration * 2/3r }

      used_duration += 2 * note_duration * 2/3r
    end

    duration_diff = gdv[:duration] - used_duration
    if duration_diff >= note_duration
      gdvs << gdv.clone.tap { |gdv| gdv[:grade] += (last = 1); gdv[:duration] = duration_diff / 2 }
      gdvs << gdv.clone.tap { |gdv| gdv[:grade] += (last = 0); gdv[:duration] = duration_diff / 2 }

    elsif duration_diff > 0
      gdvs[-1][:duration] += duration_diff / 2
      gdvs[-2][:duration] += duration_diff / 2
    end

    super gdvs, base_duration: base_duration, tick_duration: tick_duration
  else
    super
  end
end