Einfache Harmonieanalyse

Begonnen von Arnold, Mittwoch, 13. Februar 2019, 10:49

« vorheriges - nächstes »

Arnold

Hallo,

ich wollte (auch um eine aus Einzelstimmen abgeschriebene Partitur zu kontrollieren - beim Abschreiben sind mir schon einige offensichtliche Fehler, am häufigsten vergessene Versetzungszeichen, aufgefallen) zu einer Partitur eine keine Harmonieanalyse in einer Spezialnotenzeile hinzufügen.
Mein Konzept:

  • 5 Linien der Zeilen repräsentieren die schwaren Klaviertasten einer Oktave
  • Es werden die jeweils klingenden Töne gezählt, ohne Berücksichtigung der Oktave, und als »Zahlen-Notenköpfe« in diese Notenzeile eingetragen
  • Werden Akkordmuster erkannt, dann färbe ich die Notenköpfe entsprechend ein - oder füge einen Kennzeichnungstext unterhalb dieser Notenzeile ein - und der erkannte Grundton wird durch Fettdruck hervorgehoben
  • Als »kleine« Harmonieanalyse wird der Baßton (tiefster Klang) weder gesucht noch ausgewertet

Um nicht gleich Score-weit alle Töne einzusammeln und gemäß \transposition an den Analyse-Staff zu übergeben, habe ich erst einmal alle melodischen Einzelstimmen richtig transponiert zu einer Simultanen Musik im Analyse-Staff zusammengefaßt.
Und, da es wohl nur mit einem Engraver vernünftig zu lösen ist, habe ich versucht solch einen zu programmieren.
Ich scheitere beim Versuch, die Notenköpfe zu erstellen. Lilypond stürzt (unter Win7) ab, Fehlercode = Windows Error Level = -1073741819 = hexadezimal D0000005.
Gleiches Verhalten in den Versionen 2.18.2 und 2.19.82.

Meine Test-Datei (nicht ganz klein):
\version "2.18.2"
% \version "2.19.82"

\include "analysis-engraver.ly"
%%%%% WORKAROUND: (first step)
% \include "analysis-output-engraver.ly"

ClA = {
  \key f \major
  f4 g a2
  bes8 ces' g bes ces' a bes ces'
  f1 f'
}

Ve = {
  \key d \major
  <fis' a'>2.-> a'4-.
  b'1\trill
  <f' a'>4 <fis' a' c'''> <f' as''> <f' as' b'>
  <fis' a' b'>4 <fis' a' c' e'> <fis' a' c' es'> <fis' a' cis'>
}

Musik = <<
  \new Staff { \transposition a \ClA }
  \new Staff { \transposition c' \Ve }
>>

AnalyseMusik = <<
  \transpose c' a \ClA
  \Ve
>>

\score {
  << \Musik
     \new Staff \with {
       \remove Accidental_engraver
       \remove Clef_engraver
       \remove Key_engraver
       \remove Time_signature_engraver
       \remove Dot_column_engraver
       \remove Dots_engraver
       % \remove Script_engraver
       \remove Multi_measure_rest_engraver
       % \remove Text_engraver
       \remove Tie_engraver
       %%%%% DOES NOT WORK! Premature exist. (Windows Error Level = -1073741819
       % there is a boolean in line 62 of »analysis-engraver.ly«
       %    to turn off the note head creation, which initiates the problem.
       \consists #Analysis_engraver
       %%%%% WORKAROUND: first step - export into a file
       % \consists #(Analysis_output_engraver "Harmonie_analyse.ily")
     } \new Voice \with {
       \remove Accidental_engraver
       \remove Dynamic_engraver
       \remove Dot_column_engraver
       \remove Dots_engraver
       \remove Beam_engraver
       \remove Multi_measure_rest_engraver
       \remove Note_head_line_engraver
       \remove Phrasing_slur_engraver
       \remove Repeat_tie_engraver
       \remove Rest_engraver
       \remove Slur_engraver
       \remove Stem_engraver
       \remove Text_engraver
       \remove Tie_engraver
       \remove Script_engraver   % scheint nicht zu funktionieren!
       \remove Script_column_engraver
       \remove Script_row_engraver
       %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
       \remove Note_heads_engraver
     } {
       % die fünf Linien entsprechen den schwarzen Tasten einer Klaviatur
       \stopStaff
       \override Staff.StaffSymbol.line-positions = #'(0 4 10 14 18)
       \startStaff
       \AnalyseMusik
     }
     %%%%% WORKAROUND (second step): include the generated ILY file as a new Staff
     % \include "Harmonie_analyse.ily"
  >>
}


und die angegeben Inlcude-Datei »analysis-engraver.ly« (noch weniger gekürzt):
%{
analysis-engraver.ly
%}

#(define (Analysis_engraver ctx)
  (let ((pitch-list (list '() '() '() '() '() '() '() '() '() '() '() '()))
        ;                   c  cis  d  es  e   f   fis g   as  a   b   h
        (event-list (list '() '() '() '() '() '() '() '() '() '() '() '()))
        (is-main-mom #t)
        (current-main-mom -1)
        (note-started #f)
       )
   `((start-translation-timestep
        . ,(lambda (trans)
             (let* ((current-mom (ly:context-current-moment ctx))
                    (main-mom (ly:moment-main current-mom))
                    (grace-mom (ly:moment-grace current-mom)))
                (set! is-main-mom (eq? grace-mom 0))
                (if is-main-mom
                 (begin
                  ; (for-each display (list "\nstart " main-mom " G " grace-mom "\n"))
                  (set! current-main-mom (ly:make-moment main-mom))
                  (for-each display (list "\ncurrent PitchList at " current-main-mom " :"))
                  (for-each (lambda (pl)
                      (for-each display (list "\nPL-Line." (length pl) " = " pl ))
                    ) pitch-list)
                  (set! pitch-list (map (lambda (pe)
                    (filter (lambda (ts) (ly:moment<? current-main-mom ts)) pe)) pitch-list))
                  (display "\nafter terminated tones are removed:")
                  (for-each (lambda (pl)
                      (for-each display (list "\nPL-Line." (length pl) " = " pl ))
                    ) pitch-list)
                  (display "\n")
                ))
             )
           )
     )
     (listeners
      (note-event
         . ,(lambda (trans ev)
              (if is-main-mom
               (let* ((pit (ly:event-property ev 'pitch))
                      (len (ly:event-property ev 'length))
                      (end-at (ly:moment-add len current-main-mom))
                      (st (modulo (ly:pitch-semitones pit) 12))
                     )
                ; (for-each display (list "\nlength " len " pitch " pit "\n"))
                (for-each display (list "\nadd semitone " st " which ends at " end-at "\n"))
                (list-set! pitch-list st (cons end-at (list-ref pitch-list st)))
                (list-set! event-list st ev)
                (set! note-started #t)
              ))
            )
     ))
     (stop-translation-timestep
        . ,(lambda (trans)
             (for-each display (list "\nend timestep\n"))
             (if note-started
              (let ((index 0))
               (for-each (lambda (pl)
                  (if (and (not (null? pl))
                           #t) ; this section fails! You may use this boolean to turn it on/off
                   (let* ((usage-count (length pl))
                          (note-head (ly:engraver-make-grob trans 'NoteHead (ly:event-deep-copy (list-ref event-list index)))))
                    (ly:grob-set-property! note-head 'staff-position (- (* 2 index) 4))
                    ; (ly:grob-set-property! note-head 'duration-log 2)
                    (ly:grob-set-property! note-head 'stencil ly:text-interface::print)
                    (ly:grob-set-property! note-head 'text (make-simple-markup (number->string usage-count)))
                  ))
                  (set! index (1+ index))
                 ) pitch-list)
               (set! note-started #f)
             ))
           ) 
     )
    )
))


Immerhin habe ich einen Workaround für mich gefunden:
Ich mache das ganze in zwei Schritten.
Mit der ersten LY-Datei wird anstelle der Notenköpfe eine neue Lilypond-Datei mit einer kompletten Staff-Definition geschreiben,
diese muß dann nur per \include als weitere Notenzeile an die Ziel-Partitur angehängt werden. Dazu als Dateianlage mein »analysis-output-engraver.ly«.

Hat da jemand noch gute Vorschläge für mich?
Vielleicht ist aber auch der Aufwand, eine richtige »single pass«-Lösung zu erstellen viel zu aufwändig im Vergleich zu meiner aktuellen »two runs«-Lösung.

Arnold

harm6

Hallo Arnold,

mein Problem ist ich weiß nicht wie es nachher aussehen soll, insoweit bin ich unsicher, ob meine Gedanken nicht völlig am Thema vorbei gehen ...

Aber zumindest soviel:

'stop-translation-step' scheint zu spät zu sein, um ein neues grob anzulegen. Das gibt zumindest bei mir immer einen segfault.

Minimal:

\version "2.19.82"

foo_Engraver =
#(lambda (context)
  (let ((evt #f))
   `((listeners
      (note-event
         . ,(lambda (trans ev) (set! evt ev))))
      (stop-translation-timestep
        . ,(lambda (trans) (ly:engraver-make-grob trans 'NoteHead evt))))))
         
       
\new Voice \with { \consists \foo_Engraver } { c''1 }


Wenn ich aber in Deinem engraver 'stop-translation-step' durch 'process-music' ersetze klappt es erst mal.
Ich bezweifel aber, daß dann schon alles gesammelt ist, was Du gern haben möchtest.

Vielleicht hilft es aber schon weiter.


Gruß,
  Harm


Arnold

ZitatIch bezweifel aber, daß dann schon alles gesammelt ist, was Du gern haben möchtest.
Ja, Harm,
aber zumindest dieses Problem ließe sich wohl durch ein zweistufiges Vorgehen lösen:

  • Bei »process-music« den Notenkopf anlegen (falls es der erste dieser oktavfreien Tonhöhe ist).
  • Bei »stop-translation-timestep« dann das text-Attribut mit der Anzahl der gefundenen Töne ausfüllen.
alternativ:
alle zwölf Notenkopf-Grobs in »process-music« anlegen, und die nicht benötigten bei »stop-translation-timestep« wieder entfernen.

Außerdem habe ich beobachtet, daß eine temporäre parallele Musik » << { ... } \\ { ... } >> « neue Voices erstellt, für die dann auch ettliche \remove und \override hinzugefügt werden müßten - oder ein entsprechend größerer Aufwand in die Definition neuer Kontexte gesteckt werden müßte.
Dann kann man wohl auch gleich an die große Lösung herangehen, und die Note-Events Score-weit aus allen Voices sammeln, um sie dann in diesem Extra-Staff auszuwerten und darzustellen.

Also werde ich wohl vorerst noch bei meinem Workaround bleiben - des absehbaren Aufwands wegen.

Arnold

harm6

Hallo Arnold,

da ich Deine Harmonieanalyse nicht wirklich verstehe, weder nach Bild noch nach Beschreibung, war ich sehr zurückhaltend mit weiteren Vorschlägen.
(Zugegeben - ich hab' mich nicht wirklich reingekniet oder jede einzelne Codezeile nachverfolgt ...)

Noch ein paar Anmerkungen

(1)
Zitat
    Bei »process-music« den Notenkopf anlegen (falls es der erste dieser oktavfreien Tonhöhe ist).
    Bei »stop-translation-timestep« dann das text-Attribut mit der Anzahl der gefundenen Töne ausfüllen.

Ja, das kam mir auch in den Sinn...

(2)
Zitat
Dann kann man wohl auch gleich an die große Lösung herangehen, und die Note-Events Score-weit aus allen Voices sammeln, um sie dann in diesem Extra-Staff auszuwerten und darzustellen.

Eigentlich wundert es mich ein wenig, daß Du das nicht von vornherein versucht hast.
Natürlich muß man sich dann mehr um die (ich sag mal) Buchhaltung kümmern.

(3)
Du verwendest die list-syntax. Hab ich auch gemacht selbst nachdem 'make-engraver' schon lange erhältlich war, einfach weil das Geschen besser nachvollziehbar ist, imho.
Mittlerweile verwende ich 'make-engraver' (fast) immer.
Wenn man die einzelnen Teile einer engraver-Definition prinzipiell verstanden hat, ist es durchaus sehr zu empfehlen.
Da hab ich bei Dir keine Zweifel (bei mir selbst bin ich da nicht so sicher lol )


Gruß,
  Harm






Arnold

Ja, Harm,

auch ich habe da durchaus noch Verständnisprobleme mit dem Scheme Engraver:

  • Wie tauschen mehrere Engraver Informationen untereinander aus?
    Nun, die einfachste Methode sind Variablen in einem Kontext.
  • Wie wird das GROB-Netz in den Engravern aufgebaut? Wie kann es umstrukturiert werden?
  • Welche Restritkionen gibt es, wann welche Aktionen ausgeführt werden können?
    (ly:engraver-make-grob gerenell nicht bei stop-translation-timestep ausführbar?)
  • Wie übersetzt man einen (jeden vorhandenen) in C++ geschriebenen Engraver in einen Scheme-Engraver?

Und die »alte« list-syntax habe ich benutzt, weil ich da Engraver-lokale Variablen einfacher (bzw. übersichtlicher) definieren konnte.


Und, zur Info, noch eine kleine Beschreibung, was ich da mache:
  • Ich zähle die jeweils zu einem Zeitpunkt klingenden Töne, aber die Oktave berücksichtige ich nicht.
  • Somit gibt es zu jedem Zeitpunkt einen »Vektor«, ein Feld mit zwölf Ganzzahlen. Also die erste für C, die zweite für Cis, ... die zwölfte für H.
  • Übereinander gestapelt schreibe ich diese Zählnummern als Ausgabe unter das Notensystem, natürlich lasse ich die Nullen weg, nur deren Platz bleibt belegt.
  • Betrachet man den »chromatischen Tonklangvektor« als BOOLEAN, also entweder Null oder mindestens eine Stimme, dann ergeben sich für die Dreiklänge und übliche Akkorde bestimmte Muster. Und genau nach solchen Mustern suche ich auch manuell, wenn ich die Akkorde in einem Tonsatz herauszusuchen habe - für Dreiklänge ist die Quarte mit fünf Halbtönen Abstand ein guter Orientierungspunkt, für die Septakkorde der Ganzton mit zwei Haltönen Abstand - diese Mustersuche kann man einem Programm auch aufbürden.
So ging es mir zuerst einmal in einem konkreten Anwendungsfall darum, möglichst schnell mit der Darstellung des Ergebnisses praktische Erfahrung zu sammeln.
Erst auf diesen Erfahrungsschatz aufbauend, möglichst mit mehreren Partituren statt bisher nur mit einer einzigen, möchte ich die »große Lösung« angehen.

Arnold

erich

Hallo Arnold,

Ich habe mich ausführlich mit Akkorden bzw. mit Klassen von Akkorden beschäftigt. Ich gehe, so wie Du, davon aus, dass ich Intervalle in Halbtonschritten zähle. Der Durakkord ist dann [0,4,7] aber auch [3,7,10] oder [5,9,0]. Man sollte also zu einer Normierung der verschiedenen Darstellungen kommen, um beispielsweise einen Durakkord schnell als einen solchen zu erkennen. Soche Akkorde lassen sich dadurch normieren, dass man ihre kleinste lexikografische Darstellung ermittelt: für den Mollakkord ist [0,3,7] die Norm, dass es ein Mollakkord ist, erkennt man unmittelbar aus der Darstellung; für den Durakkord ist  [0,3,8] die Norm: addiert man 4 Halbtonschritte, so erhält man [4,7,12], 12 ist eine Oktave und kann durch 0 erstezt werden, die Tonzahlen sind also modulo 12 zu reduzeiren; man erhält dann [4,7,0] bzw. monoton steigend geordnent [0,4,7], wie schon eingangs gesagt ist das der Durakkord.

Weitere Ausführungen kannst du auf meiner Seite
https://meyerich.pythonanywhere.com/Tonvektoren
nachlesen.
Gruß
Erich