Positioning tuplet numbers close to kneed beams

If you want to move tuplet numbers close to kneed beams on a single staff or between staves, this workaround automates the process. It ignores tuplets on ordinary beams or with visible brackets.

Important: to work properly, this method must be used with manual beaming.

Two functions are provided. The first moves the number along the Y-axis, and is called like so: \override TupletNumber #'Y-offset = #kneed-beam. The second function centers the number horizontally: \override TupletNumber #'X-offset = #center-on-beam.

\version "2.24.0"

%% http://lsr.di.unimi.it/LSR/Item?id=646

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% A function to position tuplet numbers next to kneed beams on a single
%% staff and between staves. Will ignore tuplets on ordinary beams and
%% with visible brackets.
%%
%% Usage: \override TupletNumber.Y-offset = #kneed-beam
%%
%% You must use manual beaming for this function to work properly.
%%
%% An additional function, called with a separate override (see below), will
%% horizontally center the tuplet number on the kneed beam.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

#(define (kneed-beam tuplet-number)
   (let* ((tuplet-bracket (ly:grob-object tuplet-number 'bracket))
          (first-note (ly:grob-parent tuplet-bracket X))
          (first-stem (ly:grob-object first-note 'stem))
          (beam (ly:grob-object first-stem 'beam)))

     (if (and (ly:grob? beam) ; beam on first note?
              (ly:grob-property beam 'knee) ; is it kneed?
              (interval-empty? (ly:grob-property tuplet-bracket 'Y-extent))) ; visible bracket?
         (let* ((stems (ly:grob-object beam 'stems))
                (closest-stem (nearest tuplet-number stems))
                (direction-first-stem (ly:grob-property first-stem 'direction))
                (direction-closest-stem (ly:grob-property closest-stem 'direction))
                (beaming-near-number (car (ly:grob-property closest-stem 'beaming)))
                (beam-multiplier
                 (if (= direction-closest-stem UP)
                     (length (filter positive? beaming-near-number))
                     (length (filter negative? beaming-near-number))))
                (beam-ends (ly:grob-property beam 'positions))
                (mid-beam-Y (/ (+ (car beam-ends) (cdr beam-ends)) 2)) ; mid-beam Y-coordinate
                (number-height (ly:grob::stencil-height tuplet-number))

                ;; inital value of Y-offset (will cause number to overlap beam slightly)
                (correction
                  (- mid-beam-Y
                    (if (= direction-closest-stem UP)
                        (car number-height)
                        (cdr number-height))))
                (beam-width (ly:grob-property beam 'beam-thickness))
                (beam-gap (* 0.5 (ly:grob-property beam 'gap)))
                (beam-padding 0.2)) ; change to move number closer or farther from beam

           ;; refinement of initial value of Y-offset
           (cond
             ((= direction-first-stem direction-closest-stem DOWN)
              (- correction
                (* 0.5 beam-width)
                beam-padding))

             ((= direction-first-stem direction-closest-stem UP)
              (+ correction
                (* 0.5 beam-width)
                beam-padding))

             ((and (= direction-first-stem DOWN) (= direction-closest-stem UP))
              (+ correction
                (* beam-multiplier (+ beam-gap beam-width))
                (* 0.5 beam-width)
                beam-padding))

             ((and (= direction-first-stem UP) (= direction-closest-stem DOWN))
              (- correction
                (* beam-multiplier (+ beam-gap beam-width))
                (* 0.5 beam-width)
                beam-padding)))))))

%% find the stem closest to the tuplet-number
#(define (nearest tuplet-number stems)
  (let* ((refp (ly:grob-system tuplet-number))
         (X-coord (interval-center (ly:grob-extent tuplet-number refp X)))
         (closest (ly:grob-array-ref stems 0)))
    (let lp ((x 1))
     (if (<= (abs (- X-coord
                    (ly:grob-relative-coordinate
                      (ly:grob-array-ref stems x) refp X)))
             (abs (- X-coord
                    (ly:grob-relative-coordinate closest refp X))))
         (set! closest (ly:grob-array-ref stems x)))
     (if (< x (1- (ly:grob-array-length stems)))
         (lp (1+ x))
         closest))))

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% A function which horizontally centers a tuplet number on a kneed beam.  May
%% be used in conjunction with the earlier function.
%%
%% Usage: \override  TupletNumber.X-offset = #center-on-beam
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

#(define (center-on-beam tuplet-number)
  (let* ((tuplet-bracket (ly:grob-object tuplet-number 'bracket))
         (first-note (ly:grob-parent tuplet-bracket X))
         (first-stem (ly:grob-object first-note 'stem))
         (beam (ly:grob-object first-stem 'beam)))

    (if (and (ly:grob? beam)
             (ly:grob-property beam 'knee)
             (interval-empty? (ly:grob-property tuplet-bracket 'Y-extent)))
      (let* ((refp (ly:grob-system tuplet-number))
             (number-X (interval-center (ly:grob-extent tuplet-number refp X)))
             (beam-center-X (interval-center (ly:grob-extent beam refp X))))
    
        (- beam-center-X number-X)))))

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% END FUNCTIONS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% EXAMPLE %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
top = \change Staff = "1"
bottom = \change Staff = "2"

music = \relative c {
  \override Beam.auto-knee-gap = #1
  \tupletSpan 4
  \tuplet 3/2 {
    \bottom c8[ g' \top e']
    c'[ e, \bottom g,]
    \top e''[ \bottom c,, \top g'']
  }
  \tuplet 5/4 {
    \bottom c,,16[ \top g'' e' \bottom g,, \top c']
  }
}

\score {
  \new PianoStaff <<
    \new Staff = "1" {
      s1^"Before:"
      s1^"After:"
    }
    \new Staff = "2" {
      \clef bass
      \music
      \bar "||"
      \override TupletNumber.Y-offset = #kneed-beam
      \override TupletNumber.X-offset = #center-on-beam
      \music
    }
  >>
}