Mein erstes mittelhochdeutsches Hörbuch – Teil 6

Dies ist der sechste Teil des Tutorials. Der Beginn findet sich hier.

Bitte nochmal, mit mehr Gefühl!

Es gibt noch ein paar Aufgaben, denen wir uns widmen wollen:

  1. In einigen wenigen Fällen nimmt Hedda unsere Regeln zu genau und spricht deshalb Wörter mit einer falschen Aussprache aus.
  2. Ansonsten ist ihre Aussprache der mittelhochdeutschen Laute weitgehend korrekt, merkwürdig klingen allerdings ihre Akzentuierungen: Oft betont sie Wörter auf der falschen Silbe und manchmal wird es dann sehr schwer, den Sinn eines Verses zu verstehen.
  3. Wir werden Hedda sicher nicht beibringen können, den Text emotional zu interpretieren, aber etwas weniger monoton und seelenlos könnte ihr Vortrag doch sein.
  4. Schließlich die Satzzeichen: Bevor sie zu lesen beginnt, zeigt uns Hedda weiterhin vier Zeichen an, die ihr unbekannt sind. Es handelt sich um die Satzzeichen Punkt, Komma, Doppelpunkt und Semikolon. Wir sollten uns entscheiden, ob sie die weiterhin überlesen soll.

Auf diesen letzten Punkt können wir als erstes eingehen. Selbstverständlich handelt es sich bei den Satzzeichen, die wir im Text vorfinden um Eingriffe „moderner“ Editoren (des 19. Jahrhunderts). Im mittelalterlichen Manuskript waren sie nicht vorhanden. Die neuzeitlichen Bearbeiter geben durch die Interpunktion Fingerzeige, die beim Textverständnis helfen sollen. Wir müssen nicht zu kritisch sein und ihre Maßnahmen in Frage stellen, sondern können uns für unsere Zwecke dem Sachverstand der Experten anvertrauen. Die nun einmal im Text vorhandenen Zeichen können an Hedda weitergeben werden, sobald wir sie in die UPS-Notation überführt haben. Je nach dem für Hedda voreingestellten kulturellen Muster (Hochdeutsch/Deutschland), wird sie ihre Satzbetonung an der Interpunktion ausrichten. Bei einem Fragezeichen am Satzende hebt sie die Stimme, bei einem Punkt wird sie sie senken.

Wir fügen also unserem LexikonDerBuchstabenErsetzung ein paar Einträge hinzu. Auch wenn diese in unserem Textausschnitt gar nicht vorkommen, geben wir auch (für zukünftige Verwendungen?) die UPS-Notation für Frage- und Ausrufezeichen an. Doppelpunkt und Semikolon sind hingegen zu häufig im Textausschnitt verwendet worden, als dass wir sie ignorieren wollen. Da für sie kein UPS-Äquivalent existiert, wählen wir nach Belieben einen Ersatz, mit dem wir für uns die Frage beantworten, wie sich ein Doppelpunkt anhört (Wie ein Punkt):

            // Satzzeichen:
            ["."]="_.",
            [","]="_,",
            [":"]="_.",
            [";"]="_,",
            ["?"]="_?",
            ["!"]="_!",

Nachdem wir uns (ein wenig) um die Satzmelodie gekümmert haben, widmen wir uns als nächstes der Wortbetonung. Daran, dass das Wortformen des Deutschen meist anfangsbetont sind, hat sich seit mittelhochdeutscher Zeit nichts geändert. Wenn wir einfach jedem Wort, das wir Hedda zu lesen geben, gleich zu Beginn von FindeAussprache die Zeichenfolge [S1 ] voranstellen, wirkt Hedda Lesung druckvoller und energischer, ansonsten ändert sich jedoch nur wenig:

public string FindeAussprache(string Wort)
        {
            string Aussprache = "S1 ";

Noch deutlicher sind jetzt allerdings die Wörter auszumachen, für die sie eine völlig falsche Akzentuierung wählt. Sie übertreibt! Uns erinnert sie damit möglicherweise an jemand, der gerade eine Fremdsprache erlernt und sich jetzt ein wenig zu sehr bemüht, in der ihm fremden Rhythmik zu sprechen. Am Auffälligsten hören wir die Fehler bei Wörtern mit einer Vorsilbe, die als Präfix fungiert: ge-, be-, ver-, zer- und so weiter:

Um ihr das auszutreiben, werden wir bei jedem Wort überprüfen, ob eine der als Vorsilben gebräuchlichen Buchstabenfolgen an seinem Anfang zu finden ist. Es bietet sich an, die potentiellen Vorsilben in einer Liste zu sammeln. Wir sollten jedoch überlegen, ob wir nicht wieder ein Lexikon verwenden, in dem Hedda dann gleich die für sie lesbare Lautschrift nachschlagen kann. Die erneute Verwendung eines Lexikons mag auf den ersten Blick überflüssig erscheinen. Haben wir uns  nicht bereits darum gekümmert, alle notwendigen Informationen bereit zu stellen? Welche Laute in den potentiellen Vorsilben enthalten sind, ermitteln wir problemlos, indem wir jeden ihrer Buchstaben im LexikonDerBuchstabenErsetzung nachschlagen. Und mit welcher Betonung die präfigierten Worte zu lesen sind, dürfte sich sehr leicht bestimmen lassen.  Die vor das Wort gesetzten Silben werden nicht betont, der Hauptakzent liegt auf der folgenden (Stamm-) Silbe. Dieses Schema sollte sich auch ohne ein weiteres Lexikon automatisieren lassen.

Dieser Einwand ist berechtigt. Während wir jedoch weitere Tipparbeit durch den Einsatz eines Lexikons haben, ersparen wir Hedda bei jeder Verwendung desselben Arbeit. Arbeit, die sich in Zeit umrechnen lässt! Davon abgesehen, wird tatsächlich das e in einer Nebensilbe anders (murmelnder) ausgesprochen, als in einer, die Bedeutung und Betonung trägt. Dem können wir so Rechnung tragen. Außerdem erleichtert uns ein Lexikon den Umgang mit dem gar nicht so seltenen Sonderfall, der eintritt, wenn das verneinende un- vor ein Wort tritt, das bereits ein Präfix besitzt (un-be-kant). In diesen Fällen wird un- mit einer Betonung gelesen (ún-be-kánt), ansonsten wie die anderen Präfixe (mit Ausnahmen!) unbetont (un-réh-te). Wir begnügen uns mit den Präfixen, die wir in unserem Text finden und erstellen folgendes Lexikon, das wir in die Klasse Hedda.cs einfügen:

 private Dictionary<string, string> LexikonDerPräfixe = new Dictionary<string, string>
        {
            ["be"] = "B EX",
            ["ge"] = "G EX",
            ["ver"] = "F EHX",
            ["zer"] = "T + S EHX",
            ["en"] = "EX N",
            ["unbe"] = "S1 UH N . B EX",
            ["unge"] = "S1 UH N . G EX",
            ["unver"] = "S1 UH N . F EHX",
            ["unzer"] = "S1 UH N . T + S EHX"

        };

Der Punkt [.] markiert in der UPS-Lautschrift eine Silbengrenze. [S1 UH N . B EH] gibt demnach an, dass unbe aus zwei Silben besteht, von denen die eine mit der Lautung [UH N], die zweite mit [B EX] wiedergeben wird. Auf der ersten der beiden liegt eine Betonung [S1]. Falls Hedda in FindeAussprache eine Vorsilbe identifiziert, kann sie folgende Regel anwenden, mit der sie unsere Defaultfüllung von Aussprache (Aussprache=“S1 „) überschreibt, die für alle Wörter im Text eine Anfangsbetonung annimmt:

Aussprache = LexikonDerPräfixe[Präfix] + " . S1 ";

Wir haben somit die Aussprache zu Beginn eines Wortes ermittelt und festgelegt, dass die nächste, folgende Silbe betont auszusprechen ist. Mit dieser können wir jetzt fortfahren und nun die UPS-Kodierung für die Buchstaben im Wort hinter dem Präfix ermitteln:

Wort = Wort.Substring(Präfix.Length);

Vorsicht!!

ez ist mîn bete und ouch mîn rât,

Der Blick in den Text lehrt uns, dass nicht in jedem Wort, das mit be beginnt, be als Präfix verstanden werden darf! bete wird auf der ersten Silbe betont. Andere mittelhochdeutsche Worte beginnen ohne jede Präfigierung mit einem  ge: geste, gern.

Einige sehr häufige Wortformen dieser Art nehmen wir in das Lexikon auf, in welchem Hedda ganze Wörter und die ihnen zugeordnete Aussprache findet. Weiterhin werden wir eine Minimalzahl für die Buchstaben festlegen, die auf die Vorsilbe im Wort folgen. Und wir verlangen, dass dieses mindestens eine weitere Silbe enthält. Mit diesen beiden Methoden wird es uns nicht gelingen, alle Fehllesungen auszumerzen. Durch eine Zählung der Buchstaben und der Silben eines Wortes können wir aber in jedem Fall eine Vorauswahl vornehmen:

Kein Wort kann eine Vorsilbe enthalten, die länger ist, als es selber und einsilbige Wörter enthalten mit Sicherheit keine Vorsilbe!

Um die Silben zählen zu können, greifen wir auf eine weitere Bibliothek von C# zurück, die wir in Hedda.cs einbinden. Wir fügen eine using-Anweisung am Anfang der Datei hinzu:

 using System.Text.RegularExpressions;

Wir können jetzt sogenannte reguläre Ausdrücke benutzen. Diese sind in einer speziellen Syntax verfasst, deren Aufgabe es ist, Muster für Zeichenketten zu beschreiben. Wir können mit ihrer Hilfe in einem Text nach allen Buchstaben oder Buchstabenfolgen suchen, die dem gewünschten Muster entsprechen. Um beispielsweise anzuzeigen, dass aus einer Gruppe von Zeichen jedes gleichermaßen meine Suchkriterien erfüllt, schließe ich die Gruppe in eine eckige [] Klammer ein. In einer geschwungenen {} Klammer kann ich sodann angeben, wie viele dieser Zeichen mindestens und höchstens hintereinander vorkommen sollen. Die Silbenzahl können wir auf folgende Weise schnell mit einem regulären Ausdruck ermitteln:

       private int Silbenzahl(string Text)
        {
            Regex re = new Regex(@"[aâeêiîoôöuûüáéíóúàèìòùæœ]{1,2}");
            return re.Matches(Text).Count; 
        }

Jede Silbe, so unsere Überlegung, enthält mindestens einen Vokal oder eine Abfolge von maximal zwei Vokalzeichen. Wir zählen, wie oft Vokale oder Vokalfolgen in einem Text vorkommen, um zu erfahren, wie viele Silben er enthält. Bei Wörtern wie leier oder meien werden von uns korrekt zwei Silben gezählt.

Mithilfe eines regulären Ausdrucks können wir auch leicht die Präfixe in einem Wort bestimmen. Durch ^ signalisieren wir, dass wir nur Zeichen am Beginn eines Textes in unsere Suche aufnehmen. Um anzuzeigen, dass mehre Abfolgen von Buchstaben gleichwertig unsere Suchkriterien erfüllen, stellen wir zwischen sie ein |, dieses wird als (logisches) oder interpretiert. Mit ^ver|^zer suchen wir also nach ver oder zer am Anfang eines Wortes. Wenn wir den Ausdruck klammern, können wir das einleitende Zeichen auf den gesamten Inhalt der Klammer beziehen: ^(ver|zer). Die Klammerung erfüllt noch einen weiteren Zweck: Wenn wir später wissen wollen, welche Zeichenfolge denn tatsächlich in einem Wort vorkam, auf das wir die Suche angewendet haben, ist diese für uns in einer Liste gespeichert. Um bei mehreren Klammerungen den Überblick nicht zu verlieren, können wir den Elementen der Liste sogar einen Namen geben. Mit ^(? <Präfix>ver|zer) haben wir unserem Suchergebnis den Namen Präfix zugeordnet.

Den regulären Ausdruck müssen wir nicht komplett per Hand eingeben. Mit Join() lassen sich Elemente einer Liste zu einer Zeichenkette zusammenführen, die durch ein Zeichen, das wir frei bestimmen, getrennt sind. Der Suchauftrag wird linear ausgeführt. Wir wollen sicherstellen, dass zuerst auf unbe geprüft wird und nur, wenn dieses nicht am Anfang des Wortes steht, auch auf un oder be. Die Sortierung der Stichwörter unseres Lexikons nach ihrer Länge führen wir nach dem bekannten Schema durch. So erhalten wir den passenden regulären Ausdruck für unsere Suche nach den präfigierenden Vorsilben:

List<string> Präfixe = LexikonDerPräfixe.Keys.ToList();
Präfixe.Sort((Präfix1, Präfix2) => Präfix2.Length.CompareTo(Präfix1.Length));
string Suche = "^(?<Präfix>"+String.Join("|",Präfixe)+")";

Ganz ähnlich können wir auch am anderen Ende der Wörter verfahren: Mehrsilbige mittelhochdeutsche Wörter enden meist in einer unbetonten Silbe, deren Silbenkern als e geschrieben, aber als Murmellaut gesprochen wird. In den ersten fünf Versen unseres Textes finden wir als Beispiele: gemer-ken geden-ke wî-se al-ten zî-ten vol-ge dûh-te.

Jede dieser Silben wird von einem Konsonanten eingeleitet, es ist aber in Worten wie lei-er auch möglich, dass sie einen offenen linken Rand aufweist. Als mögliche Silbenendungen kommen außerdem noch ec, es, ez, er, ers, ern, ent, et, el und els in Frage. Wir wollen uns diese Möglichkeiten veranschaulichen, bevor wir unsere Ersetzungsroutine implementieren:

Endsilbe
Wort Wortbeginn linker Rand Murmellaut rechter Rand
alte al- t e
ziten  zi-  t  e  n
hoerent hoe- r e nt
leier lei- e r

Die Abfrage am Ende des Wortes gestaltet sich etwas schwieriger, als am Anfang. Wir können sie sinnvollerweise erst vornehmen, nachdem wir die Laute der Aussprache, inklusive des zu ersetzenden e, ermittelt haben. Unser regulärer Ausdruck operiert also auf der UPS-Notation. Die Neuformulierung der Aussprache für das Wort werden wir größtenteils aus Elementen, die wir mithilfe des regulären Ausdrucks in der Ausgangszeichenkette ermitteln, zusammen“basteln“. Wir werden wieder benötigte Wortbestandteile deshalb unter einem von uns festgelegten Namen erfassen.  Nicht vergessen dürfen wir schließlich die Möglichkeit, dass am Ende des Wortes ein Satzzeichen steht.  Folgender reguläre Ausdruck erfüllt all unsere Bedingungen und Wünsche:

(?<Anfang>.+[^\.\+] )(?<Ei>E lng \+ IH )?(?<Links>(?(Ei)|(P \+ F |T \+ S |SH |[BCDFGHKLMNRSTVZ] )))EH (?<Rechts>N T |R T |L S |R S |[SZTRNK] )?(?<Satzzeichen>_[\.\,\?\!] )?$

Das ist ein Tatzelwurm, der sich vermutlich selbst ein Rätsel ist. Wir können versuchen, ihn ein wenig zu entwirren, indem wir seine Segmente durch natürliche Sprache wiedergeben und uns dabei an den Namen entlang hangeln, die wir vergeben haben:

Am Anfang eines Wortes, das unseren Bedingungen entspricht gibt es eine beliebige Abfolge von Zeichen, deren vorletztes (vor einem Leerzeichen) aber kein Punkt und kein Plus sein darf. Der Punkt dient in UPS der Silbentrennung.  Das + verbindet UPS-Zeichen zu einem gemeinsam kodierten Lautwert. Diesen wollen wir nicht trennen und damit zerstören und die Silbengrenze wollen wir ja gerade setzen. Wenn wir bei diesem Vorhaben auf einen Punkt treffen, läuft etwas falsch. . und + stellen wir einen Schrägstrich voran. Warum wir das tun, werden wir gleich erklären.

Auf den Anfang kann der Diphthong ei folgen – oder auch nicht. Das Fragezeichen hinter dem Ausdruck ist als Quantifizierer (kein- oder einmal) zu verstehen und könnte durch {0,1} ersetzt werden.

Kommen wir zum linken Rand der gesuchten Endsilbe (Links): Falls soeben ein ei  im Wort gefunden wurde, erfahren wird das durch ?(Ei), das Fragezeichen dient hier der Abfrage. Wenn die Frage bejaht wird, dann gibt es keinen linken Rand. Wenn es kein ei gibt, geht es nach dem | weiter, das für ein logisches oder steht. In diesem Fall wird der linke Rand durch einen Konsonant gebildet, der auch einmal durch mehrere, mit + verbundene, Lautzeichen bezeichnet werden kann. Da das + auch ein Quantifizierer sein kann, müssen wir durch den vorangestellten Schrägstrich signalisieren, dass wir diesmal tatsächlich das Buchstabenzeichen meinen.

Ausnahmsweise ist es nun ganz einfach: Nach dem linken Silbenrand, muss ein EH im Wort aufzufinden sein.

Rechts von diesem kann eine bestimmte Gruppe von Konsonanten oder Konsonantenabfolgen stehen. Auch der rechte Silbenrand kann allerdings leer bleiben, was wir erneut durch den Quantifizierer ? angeben.

Ach ja. Und dann könnte noch ein Satzzeichen folgen … Die Schrägstriche / sind erneut darauf zurückzuführen, dass Satzzeichen eine besondere Bedeutung in regulären Ausdrücken haben können. Dem ? sind wir schließlich oft genug begegnet.

ENDE. Nur, wenn wir jetzt am Wortende $ angekommen sind, entspricht das Wort unseren Bedingungen.

War doch gar nicht so schwer, oder? Jetzt muss nur noch aus diesen Bestandteilen eine neue Zeichenkette für Hedda gebildet werden.

Die mithilfe von regulären Ausdrücken vorgenommenen Änderungen betreffen bisher nur die mehrsilbigen Wörter des Textes. Alle Kurzwörter, also diejenigen, die nur aus einer Silbe bestehen, werden betont ausgesprochen. Nicht zuletzt deshalb wirkte Heddas letzte Lesung so kraftvoll auf uns! Sie liest mit gleichem Verve auch jede Partikel, jedes Pronomen, die Formen des bestimmten Artikel und die kurzen Präpositionen. Damit sie diese Wörter mit weniger Energie von sich gibt, erstellen wir eine Liste, die wir (per copy & paste) in die Klasse Hedda.cs einfügen:

        List<string> Kurzwörter = new List<string>
        { "vor","von","der","diu","die","daz","diz","ditz",
            "den","dem","dan","und","in","im","ir",
            "an","ze","al","ez","für","ich","er","si",
            "wol","bî","hie","wi","wie","ein","niht",
            "mîn","sîn","mich","sich","doch","noch","nâch",
            "dar","zuo","ie","mit","mê", "ouch","nu","unz",
            "als","vil","wan","gar"
        };

Dann können wir für diese Wörter in FindeAussprache den Defaultwert korrigieren:

if (Kurzwörter.Contains(Wort)) Aussprache = ""; 

Nachdem wir jetzt all diese Veränderungen an FindeAussprache vorgenommen haben, sieht die deutlich umfangreichere Methode inzwischen so aus:


        public string FindeAussprache(string Wort)
        {
            string Aussprache = "S1 "; //Default
            if (Kurzwörter.Contains(Wort)) Aussprache = ""; // Wenn das Wort nicht zu den meist unbetonten Kurzwörtern gehört!
            
            /* Wir überprüfen, ob die Möglichkeit einer präfigierenden Vorsilbe besteht.
             * Dazu muss das Wort mindestens vier Buchstaben und zwei Silben aufweisen:
             */
            if (Wort.Length>4 && Silbenzahl(Wort) > 1)
            {
                
                // Wir bauen die Suchanfrage auf:

                List<string> Präfixe = LexikonDerPräfixe.Keys.ToList();
                Präfixe.Sort((Präfix1, Präfix2) => Präfix2.Length.CompareTo(Präfix1.Length));
                string Suche = @"^(?<Präfix>" + String.Join("|", Präfixe) + ")";
                
                // Wir suchen. "Treffer" enthält die Suchergebnisse

                Regex reAnfang = new Regex(Suche);
                Match Treffer =  reAnfang.Match(Wort);

                if (Treffer.Success) // Wenn es einen Treffer gibt ...
                {
                    string Präfix = Treffer.Groups["Präfix"].ToString(); // ist dieser in Präfix gespeichert
                                                                        
                    // Erneute Überprüfung: Ist das gefundene Präfix zu lang?

                    if (Wort.Length-Präfix.Length > 2 && Silbenzahl(Wort.Substring(Präfix.Length))>0)
                    {
                        // Gut. Wir ersetzen.

                        Aussprache = LexikonDerPräfixe[Präfix] + " . S1 ";
                        Wort = Wort.Substring(Präfix.Length);
                    }  
                }
            }
           
            // Präfix oder nicht, jetzt wird die Aussprache des (Rest-)Wortes gefunden
            // Dieser Teil von FindeAussprache hat sich nicht geändert.

            int i = 0; // Index des Buchstabens, an dem wir uns gerade befinden.
            while (i<Wort.Length) // Solange i < als die Wortlänge ist.
            {
                List<string> MöglicheBuchstabenFolgen; 
                MöglicheBuchstabenFolgen = LexionDerBuchstabenErsetzung.Keys // Die mhd. Buchstabenfolgen
                                            .Where(Buchstaben => Buchstaben[0] == Wort[i]) // bei denen der erste Buchstabe und der aktuelle unseres Suchbegriffes identisch sind.
                                            .ToList();                         // als Liste.
                MöglicheBuchstabenFolgen.Sort((Folge1,Folge2)=>Folge2.Length.CompareTo(Folge1.Length)); // Sortiere die Liste absteigend nach ihrer Länge.

                /* Wir gehen die Buchstabenfolgen mit dem gesuchten Anfang,
                 * die der Länge nach, absteigend sortiert sind,
                 * eine nach der anderen durch und überprüfen,
                 * ob sie sich ganz an der Stelle i in unserem Wort finden.
                 * Wenn die erste (längste) Folge identifiziert wird,
                 * brechen wir die Suche ab und ersetzen.
                 * Andernfalls suchen wir weiter.*/
                bool Gefunden = false; // 
              
                    
                foreach (string Buchstabenfolge in MöglicheBuchstabenFolgen)
                {
                   
                    if (Wort.Length-i >= Buchstabenfolge.Length && Wort.Substring(i, Buchstabenfolge.Length) == Buchstabenfolge)
                    {
                        string Ersatz = LexionDerBuchstabenErsetzung[Buchstabenfolge];
                        if (Ersatz[0]=='X')
                        {
                            if (i - 1 >= 0 && "iîeêöüäœæ".Contains(Wort[i - 1])) Ersatz = "C";
                            if (i - 2 >= 0 && Wort.Substring(i-2,2)=="iu") Ersatz = "C";
                        }

                        if (Ersatz=="T + S")
                        {
                            if (i - 1 >= 0 && "aouâôûiîeêöüäœæ".Contains(Wort[i - 1])) Ersatz = "Z"; 
                        }
                        Aussprache += Ersatz; // X += Y ist X = X + Y
                        Aussprache += ' '; //nicht das Leerzeichen vergessen!
                        i +=  Buchstabenfolge.Length;
                        Gefunden = true;
                        break; // Ein Ersatz gefunden, wir suchen nicht mehr weiter!
                    }
                
                }
                if (!Gefunden)
                {
                    // Unbekannter Buchstabe
                    // Falls noch nicht in der Liste der unbekannten Zeichen,
                    // trage ihn ein.
                    if (!UnbekannteZeichen.Contains(Wort[i])) UnbekannteZeichen.Add(Wort[i]);
                    i += 1; // Zähle Weiter
                }
            }

            // Jetzt kümmern wir uns um das Ende des Wortes:

            if (Silbenzahl(Wort) > 1)
            {
               /* Wir fragen ab, ob eine unbetonte Endsilbe existieren kann und speichern das Ergebnis
                  Der ziemlich komplizierten Abfrage in vier Variablen:
                    Anfang - speichert den Beginn des Wortes VOR der Endsilbe
                    Links - speichert den linken Rand der Silbe.
                    Rechts - entsprechend den rechten Rand.
                    Satzzeichen - nimmt ein etwaiges Satzzeichen auf.
                    */

               
                Regex reEnde = new Regex(@"(?<Anfang>.+[^\.] )(?<Ei>E lng \+ IH )?(?<Links>(?(Ei)|(P \+ F |T \+ S |SH [BCDFGHKLMNRSTVZ] )))EH (?<Rechts>N T |R T |L S |R S |[SZTRNK] )?(?<Satzzeichen>_[\.\,\?\!] )?$");

                Match Suffix = reEnde.Match(Aussprache);
               
               
                if (Suffix.Success)
                {
 
                    // Wenn unsere Suche Erfolg hatte, setzen wir das Ergebnis zusammen:
                  
                    Aussprache = Suffix.Groups["Anfang"]+"" + Suffix.Groups["Ei"] + ". "+Suffix.Groups["Links"] + " EX " + Suffix.Groups["Rechts"]+Suffix.Groups["Satzzeichen"];

                } 
        
            }
       
            return Aussprache;
        } 

Ob sich unsere Mühen gelohnt haben, können wir uns anhören, während wir den C#-Code studieren und versuchen die regulären Ausdrücke zu verstehen:

Endet ein Vers übrigens mit einem Wort, dessen letzte Silbe unbetont ist, wird er traditionell als klingend, oder (etwas chauvinistisch) als weiblich bezeichnet. Endet er hingegen auf betonter Silbe, so nennt man dies eine stumpfe, beziehungsweise männliche Kadenz. Um zu erfahren wie der Vers endet werden wir wieder einen regulären Ausdruck verwenden. Diesmal können wir wieder mit der vielleicht lesbareren mittelhochdeutschen Buchstabenschrift arbeiten:

        private bool Klingend(string Zeile)
        {
            Regex reVersende =new Regex( @"ei|[bcdfghklmnprstvwz]e[rntlsz\.,;:\?!]{0,3}$");
            if (reVersende.Match(Zeile).Success)
            {
                // Wir akzeptieren das Resultat, wenn das letzte Wort von Zeile mehrsilbig ist:
                if (Silbenzahl(Zeile.Split().Last()) > 1) return true;
            }
            return false; // Alle anderen Fälle.
        }

Wenn wir diese Methode zu Hedda.cs hinzufügen, können wir die dadurch zugängliche Information für zwei weitere kleine Verbesserungen nutzen. Als erstes entfernen wir aus Main in Program.cs die Zeile:

pb.AppendBreak(PromptBreak.Small);

Stattdessen setzen wir ganz am Ende, direkt vor der schließenden Klammer }, in der Methode LeseZeile von Hedda.cs folgende Zeile ein:

pb.AppendBreak(Klingend(Zeile)?PromptBreak.ExtraSmall:PromptBreak.Small);

Wir machen die Länge der Pause nach einem Vers dadurch jetzt abhängig vom Versausgang. Wenn er klingend endet, ist die Pause sehr kurz. Nach einer betonten Silbe ist die Pause ein bisschen länger.

Und schließlich widmen wir uns noch einmal den  Kurzwörtern: Das mittelhochdeutsche Versmaß ist gleichmäßig alternierend. Auf eine betonte Silbe folgt, abgesehen von ein paar sehr seltenen Ausnahmen, immer eine unbetonte. Und umgekehrt.

Für ein Kurzwort, das auf eine betonte Silbe fällt, bedeutet dies, das es in diesem Falle doch betont zu lesen ist. Um zu verstehen, warum dies wichtig sein kann, betrachten wir uns eine Wortform wie der. Wenn wir in  einem Text auf dieses Wort treffen, ist es meist der bestimmte Artikel. Als solcher hat es eine untergeordnete Funktion, der (Haupt-) Akzent liegt selbstverständlich auf dem Súbstantiv, dem das Wórt der vorangeht. Wenn wir unsere Betonung jedoch ändern, DER Artikel sagen, dann geben wir zu verstehen, dass wir einen ganz bestimmten, herausgehobenen Artikel meinen. dér ist dann kein Artikel mehr, sondern ein Demonstrativpronomen (in der Bedeutung: dieser, genau der).  Selbstverständlich wird der auch als  Relativpronomen mit Betonung gesprochen (der Artikel, dér). Auch in einem mittelhochdeutschen Vers kann die Betonung eines Wortes einen Bedeutungsunterschied machen und wir sollten annehmen dürfen, dass die Dichter Akzente bewusst gesetzt haben.

Wir sollten uns deshalb darum bemühen, Hedda dazu zu bringen, dem Versmaß entsprechend zu lesen. Alles, was sie dazu wissen muss, ist wie der Vers jeweils endet.  Denn am Anfang des Verses darf der Dichter ein wenig schummeln. Wenn wir jedoch vom Versende rückwärts zählen, wissen wir, dass im stumpfen Vers die zweite, vierte, sechste, im klingenden die erste, dritte, fünfte und siebte Silbe der Zeile betont sein sollte. Wer sich jetzt wundern sollte: Wir zählen selbstverständlich nach Informatikerart und  beginnen mit dem Index 0! Die Regulierung in LeseZeile lautet also:

                       if (Kurzwörter.Contains(Wort))
                        {
                            if ((SilbenImVers-Silbe) % 2 == 1 && Klingend(Zeile)) Lautung = "S1 " + Lautung;
                            if ((SilbenImVers-Silbe) % 2 == 0 && !Klingend(Zeile)) Lautung = "S1 " + Lautung;                                 
                        }

Silbe ist der Index der Silben, die Hedda bisher bearbeitet hat, inklusive des gerade bestimmten Wortes. In der Variablen SilbenImVers haben wir zuvor die Silbenzahl der Verszeile gespeichert. Mit dem Rechenzeichen % (dem Modulo) teilen wir die Differenz dieser Zahlen durch zwei und erhalten den Rest, erfahren also, ob es sich um eine gerade oder ungerade Silbenzahl vom Versende (Index: 0!) gesehen handelt. Die komplette Methode LeseZeile sieht nun so aus:

        public  void LeseZeile (string Zeile, PromptBuilder pb)
        {

            /* Los geht's Hedda!
             * Lies Wort für Wort.
             * Überprüfe, ob das Wort im Lexikon vermerkt wurde.
             * Wenn ja, dann folge der dort in Lautschrift notierten Aussprache.
             * Wenn nicht ... mach's wie immer, sprich so, wie es da "geschrieben" steht.*/

            int SilbenImVers = Silbenzahl(Zeile);
            int Silbe = 0;

            foreach (string Wort in Zeile.ToLower().Split(' '))
            {
                Silbe += Silbenzahl(Wort);
                if (Lexikon.Keys.Contains(Wort))
                {
                    //Sonderfall, der im Lexikon gefunden wurde:
                    pb.AppendSsmlMarkup(@"<phoneme alphabet=""ups"" ph=""" + Lexikon[Wort] + @""">"// Die Lautschriftvariante
                                        + Wort + "</Phoneme>");//  gefolgt von der Graphie  
                }
                else
                {
                    // Unbekanntes Wort, ermittel den Ausspracheregeln folgend die korrekte Lautung:
                    string Lautung = FindeAussprache(Wort);
                    if (Lautung != "")
                    {
                        if (Kurzwörter.Contains(Wort))
                        {
                            if ((SilbenImVers-Silbe) % 2 == 1 && Klingend(Zeile)) Lautung = "S1 " + Lautung;
                            if ((SilbenImVers-Silbe) % 2 == 0 && !Klingend(Zeile)) Lautung = "S1 " + Lautung;
                                       
                        }
                        pb.AppendSsmlMarkup(@"<phoneme alphabet=""ups"" ph=""" + Lautung + @""">"// Die Lautschriftvariante
                                            + Wort + "<Phoneme>");//  gefolgt von der Graphie  
                    }
                }
            }
            pb.AppendBreak(Klingend(Zeile)?PromptBreak.ExtraSmall:PromptBreak.Small);
                   
        }

Wir sind nun vorläufig am Ende unserer selbst gestellten Aufgabe angelangt. Denn jetzt müssen wir Heddas Lesung nur noch in eine Audiodatei umleiten. Außerdem werden wir noch einige kleine Aufräumarbeiten vornehmen und uns ein paar Gedanken darüber machen, wie eine Weiterführung unseres Projektes jenseits des Tutorials aussehen könnte. Doch vorerst hören wir uns an, was wir geleistet haben:

Und weiter geht es dann im siebten und letzten Teil unseres Projektes.

Mein erstes mittelhochdeutsches Hörbuch – Teil 5

Dies ist der fünfte Teil des Tutorials. Der Beginn findet sich hier.

Die kleine Raupe Hedda 

Wie bringen wir Hedda nun dazu sich Buchstabe für Buchstabe durch den Text zu fressen?

Ganz einfach: Wir verwenden eine Schleife, die einen Buchstabenindex i bei jedem Durchlauf weiterzählt. Wir beginnen mit dem ersten Buchstaben (in C# und anderen Programmiersprachen als 0 gezählt), dann folgt der zweite (C#: 1) und so weiter … bis keine Buchstaben mehr zu zählen sind. Was dann der Fall ist, wenn  unser Index i mit der Text- oder Wortlänge identisch ist:

        public string FindeAussprache(string Wort)
        {
            string Aussprache = "";
            int i = 0; // Index des Buchstabens, an dem wir uns gerade befinden.
            while (i < Wort.Length) // Solange i kleiner als die Wortlänge ist.
            {
                // Zähle den Index i weiter
                // Schreibe die zur Wortstelle[i] passende Lautung an das Ende von Aussprache
            }            
            return Aussprache;
        }            

Der Wert des Index i erhöht sich vermutlich in den meisten Durchläufen genau um 1, um den einen Buchstaben, der ersetzt werden soll. Doch manchmal handelt es sich bei dem Buchstaben an der Stelle i um den Anfang einer Buchstabenfolge, die im Ganzen ersetzt werden muss: ein Diphthong beispielsweise, oder der Trigraph sch für das IPA-Lautzeichen [ʃ]. i wird dann um die Länge der zu ersetzenden Folge weitergezählt (also 3 im letzten Fall). An die Zeichenkette Aussprache wird in diesem Fall die der IPA-Kodierung [ʃ] entsprechende UPS-Variante [SH ] angehängt, die sich im Lexikon unter dem Stichwort [„sch“] befindet, bzw. allgemeiner und in C# formuliert:

            if (Wort.Substring(i, Buchstabenfolge.Length) == Buchstabenfolge)
            // Wenn das Wort ab der Position i bis zur Länge der Buchstabenfolge 
            // identisch mit der Buchstabenfolge ist, dann ...
            {
                Aussprache = Aussprache + LexionDerBuchstabenErsetzung[Buchstabenfolge];
                i = i + Buchstabenfolge.Length;
            }

Was wir jetzt noch brauchen, ist eine Liste aller Lexikoneinträge, bei denen das Stichwort mit dem Buchstaben beginnt, der sich an Position i in unserem Wort befindet. Um an diese zu gelangen, verwenden wir eine für spezielle Syntax (LINQ), die C# für solche Zwecke bereitstellt:

                List<string> MöglicheBuchstabenFolgen; 
                MöglicheBuchstabenFolgen = LexionDerBuchstabenErsetzung.Keys // Die mhd. Buchstabenfolgen
                                            .Where(Buchstaben => Buchstaben[0] == Wort[i])
                                         // bei denen der erste Buchstabe und der aktuelle im Suchbegriff identisch sind.
                                            .ToList();                         // als Liste.

Wir gehen von nun an davon aus, dass Buchstabenfolgen, die sich im Lexikon der Buchstabenersetzungen auffinden lassen, auch immer als diese Folgen zu lesen sind. Ein e gefolgt von einem i ist also immer als ein ei aufzufassen, die hintereinander geschriebenen Buchstaben s,c und h immer als sch. Uns ist durchaus bewusst, dass wir mit dieser Annahme Gefahr laufen, ein paar Blumento-Pferde einzufangen. Das mittelhochdeutsche Beispiel geirren haben wir schon angeführt, das selbstverständlich als ge-irren zu lesen ist und nicht als geir-ren. Doch wenn wir uns recht erinnern, haben wir ja bereits ein anderes Lexikon angelegt, in dem wir ganze mittelhochdeutsche Wörter nachschlagen und ihre Verlautung auffinden können.

Dieses Lexikon kümmert sich um die (hoffentlich!) nicht so häufigen Sonderfälle und ansonsten sollten wir im Normalfall mit unserer Verallgemeinerung zurechtkommen. Deshalb können wir die Liste der möglichen Buchstabenfolgen mit dem gesuchten Anfangsbuchstaben der Länge nach sortieren. Dann zählen wir vom längsten Eintrag zum kürzesten abwärts, solange, bis ein Eintrag in unser Wort an die Stelle i passt. Der Eintrag darf dazu nicht länger sein, als Buchstaben nach i im Wort sind und keiner seiner Buchstaben darf im Wort an entsprechender Stelle fehlen. Stimmt alles, schlagen wir im Lexikon nach, um unserer Ausgabezeichenkette Aussprache die passende Lautung anzuhängen. Die komplette Methode FindeAussprache lautet also:

        public string FindeAussprache(string Wort)
        {
            string Aussprache = "";
            int i = 0; // Index des Buchstabens, an dem wir uns gerade befinden.
            while (i<Wort.Length) // Solange i < als die Wortlänge ist.
            {
                List<string> MöglicheBuchstabenFolgen; 
                MöglicheBuchstabenFolgen = LexionDerBuchstabenErsetzung.Keys // Die mhd. Buchstabenfolgen
                                            .Where(Buchstaben => Buchstaben[0] == Wort[i]) // bei denen der erste Buchstabe und der aktuelle unseres Suchbegriffes identisch sind.
                                            .ToList();                         // als Liste.
                MöglicheBuchstabenFolgen.Sort((Folge1,Folge2)=>Folge2.Length.CompareTo(Folge1.Length)); // Sortiere die Liste absteigend nach ihrer Länge.

                /* Wir gehen die Buchstabenfolgen mit dem gesuchten Anfang,
                 * die der Länge nach, absteigend sortiert sind,
                 * eine nach der anderen durch und überprüfen,
                 * ob sie sich ganz an der Stelle i in unserem Wort finden.
                 * Wenn die erste (längste) Folge identifiziert wird,
                 * brechen wir die Suche ab und ersetzen.
                 * Andernfalls suchen wir weiter.*/
                bool Gefunden = false; // 
                foreach (string Buchstabenfolge in MöglicheBuchstabenFolgen)
                {
                    if (Wort.Length-i >= Buchstabenfolge.Length && Wort.Substring(i, Buchstabenfolge.Length) == Buchstabenfolge)
                    {
                        Aussprache += LexionDerBuchstabenErsetzung[Buchstabenfolge]; // X += Y ist X = X + Y
                        Aussprache += ' '; //nicht das Leerzeichen vergessen!
                        i +=  Buchstabenfolge.Length;
                        Gefunden = true;
                        break; // Ein Ersatz gefunden, wir suchen nicht mehr weiter!
                    }
                
                }
               if (!Gefunden) i += 1; // Unbekannter Buchstabe, zähle weiter und tue so als sei nichts geschehen.

            }
       
            return Aussprache;
        }

Jetzt wollen wir endlich austesten, was unsere Methode FindeAussprache leisten kann und deshalb ändern wir LeseZeile so ab, dass für jedes Wort, welches sich nicht als Lexikoneintrag finden lässt, FindeAussprache aufgerufen wird:

        public  void LeseZeile (string Zeile, PromptBuilder pb)
        {

            /* Los geht's Hedda!
             * Lies Wort für Wort.
             * Überprüfe, ob das Wort im Lexikon vermerkt wurde.
             * Wenn ja, dann folge der dort in Lautschrift notierten Aussprache.
             * Wenn nicht ... mach's wie immer, sprich so, wie es da "geschrieben" steht.*/

            foreach (string Wort in Zeile.Split(' '))
            {
                if (Lexikon.Keys.Contains(Wort))
                {
                    //Sonderfall, der im Lexikon gefunden wurde:
                    pb.AppendSsmlMarkup(@"<phoneme alphabet=""ups"" ph=""" + Lexikon[Wort] + @""">"// Die Lautschriftvariante
                                        + Wort + "</phoneme>");//  gefolgt von der Graphie  
                } 
                else
                {
                    // Unbekanntes Wort, ermittel den Ausspracheregeln folgend die korrekte Lautung:
                    string Lautung = FindeAussprache(Wort);
                    if (Lautung !="")
                    pb.AppendSsmlMarkup(@"<phoneme alphabet=""ups"" ph=""" + Lautung + @""">"// Die Lautschriftvariante
                                        + Wort + "</phoneme>");//  gefolgt von der Graphie  
                }
            }
        }

Ab jetzt liest uns Hedda Wort für Wort wirkliches Mittelhochdeutsch vor! Denn was wir zu hören bekommen, ist nicht mehr ein Hochdeutsch, in dem wir nur die in dieser Aussprache zu merkwürdigen Lautungen ersetzt haben:

Der Höreindruck erinnert uns in erschreckender Eindringlichkeit daran, dass das Lexikon der Buchstabenersetzung bisher nur eine Liste von Konsonanten, einen Vokal und drei Diphthonge enthält. Es ist Zeit, es auszubauen! Dabei kann uns Hedda eigentlich helfen, indem sie über die ihr fremden Buchstaben … ähm … Buch führt, anstatt einfach still über sie hinwegzuzählen. Wir richten ihr dazu eine Liste ein, in die sie diese Zeichen einträgt und ändern den Beginn der Klasse Hedda.cs entsprechend ab:

   class Hedda
    {
        // Die Liste der von Hedda in Text vorgefundenen unbekannten Buchstaben:
        public List<char> UnbekannteZeichen = new List<char>();

        // Ein Lexion zur Aufnahme der abweichend auszusprechenden Wortformen:
        private Dictionary<string, string> Lexikon = new Dictionary<string, string>
        {
            ["rehtiu"] = "S1 R EH C . T Y lng",
            ...

Ebenso ändern wir ihr Verhalten in FindeAussprache, wenn sie auf diese Fremdkörper trifft.
Aus:

               if (!Gefunden) i += 1; // Unbekannter Buchstabe, zähle weiter und tue so als sei nichts geschehen.

machen wir:

               if (!Gefunden)
               {
                    // Unbekannter Buchstabe
                    // Falls noch nicht in der Liste der unbekannten Zeichen,
                    // trage ihn ein.
                    if (!UnbekannteZeichen.Contains(Wort[i])) UnbekannteZeichen.Add(Wort[i]);
                    i += 1; // Zähle Weiter
               }

Diese Buchstaben kann uns Hedda selber vorlesen, denn (neuhochdeutsches) Buchstabieren beherrscht sie auch. Da sie aber nicht mit jedem Zeichen zurechtkommt, lassen wir uns die Buchstaben nach einer Anpassung von Main in Program.cs auch in Schriftform im Konsolenfenster anzeigen:

        static void Main(string[] args)
        {
            SpeechSynthesizer synth = new SpeechSynthesizer(); 
            PromptBuilder pb = new PromptBuilder();
            Hedda Hedda = new Hedda();

            //Unsere Textdatei: 
           
            StreamReader Textdatei = new StreamReader("D:/Lanzelet 1 - 49.txt");

            string Zeile;

            /* Schleife:
             * Solange bis das Ende der Textdatei erreicht ist (der Befehl ReadLine() null ergibt)
             * Lies die nächste Zeile ein
             * Und übergib sie an Hedda zum Vorlesen.
             */
          
            while (( Zeile = Textdatei.ReadLine())!=null)
            {
                Hedda.LeseZeile(Zeile, pb); 
            }
           
            synth.Speak("Ich habe folgende mir unbekannten Zeichen gefunden:");
            Console.WriteLine("Ich habe folgende mir unbekannten Zeichen gefunden:");

            foreach (char c in Hedda.UnbekannteZeichen)
            {
                Console.Write(c); // Schreibe die Hedda unbekannten Zeichen ins Konsolenfenster .
                synth.Speak(c.ToString()); // und lass Hedda die Buchstaben sprechen.   
            }
            Console.ReadKey(); //Warte auf eine Zeicheneingabe, bevor Hedda mit dem Lesen des Textes beginnt.

            synth.Speak(pb);  
        }

Hedda kann uns nur helfen, Einzelbuchstaben, die sie nicht kennt, zu identifizieren. Die von ihr angegebene Liste der fehlenden mittelhochdeutschen Schriftzeichen ergänzen wir darum direkt um die Lautungen der mittelhochdeutschen Diphthonge und anderer Mehrbuchstabenkombinationen und tragen die Lautungen in unser LexikonDerBuchstabenErsetzung ein:

Dictionary<string, string> LexionDerBuchstabenErsetzung = new Dictionary<string, string>
{
            ["î"] = "I lng",
            ["iu"] = "Y lng",
            ["ei"] = "E lng + IH",
            ["ie"] = "I lng + EH",
            ["a"]="AA",
            ["â"]="A lng",
            ["ä"]="A",
            ["ae"]="EH lng",
            ["æ"] ="EH lng",
            ["c"]="K",
            ["ch"]="X",
            ["e"]="EH",
            ["ê"]="E lng",
            ["i"]="IH",
            ["o"]="O",
            ["ô"]="O lng",
            ["oe"]="EU",
            ["œ"] ="EU",
            ["ö"]="OE",
            ["ou"]="AO + UH",
            ["öu"]="OI",
            ["ph"]="P + F",
            ["sc"]="SH",
            ["sch"]="SH",
            ["u"] = "UH",
            ["û"]="U lng",
            ["uo"]="U lng + O",
            ["ü"]="YH",
            ["üe"]="Y lng + EH",
            ["w"]="v",
            ["v"]="f",
            ["z"]="T + S",
            ["zz"]="Z"
};

Bevor wir es vergessen: Hedda führt in der Liste der von ihr nicht erkannten Buchstabenzeichen auch alle Großbuchstaben auf, die es im Textausschnitt gibt. Ob ein Buchstabe groß oder klein geschrieben wird, kann man jedoch nicht hören! Wir wandeln deshalb einfach mit ToLower() jede komplette Verszeile, bevor wir sie in Worte aufspalten, komplett in Kleinbuchstaben um. Der Beginn der Leseschleife in der Methode LeseZeile der Klasse Hedda.cs lautet danach:

           foreach (string Wort in Zeile.ToLower().Split(' '))

 

Jetzt können wir die Taste F5 betätigen und gespannt die Ohren spitzen. Diesmal ist es nun wirklich, wirklich wahr: Was aus unseren Lautsprechern ertönt, ist zwar noch nicht perfekt, aber zum ersten Mal klingt es tatsächlich nach Mittelhochdeutsch! Es sollte sogar möglich sein, zu verstehen, was Hedda vorliest:

Wenn sich unsere Euphorie ein wenig gelegt hat, werden wir spätestens beim zweiten Hören noch zahlreiche kleine „Baustellen“ entdecken. Wir könnten uns allerdings deutlich besser auf diese Probleme konzentrieren, wenn Hedda nicht ganz so atemlos durch den Text hasten würde. Wir drosseln deshalb ihr ungestümes Tempo etwas, indem wir nach jeder von ihr gelesenen Verszeile eine kleine Pause einfügen. Die Leseschleife in der Methode Main der Datei Program.cs lautet also nun:

            while (( Zeile = Textdatei.ReadLine())!=null)
            {
                Hedda.LeseZeile(Zeile, pb);
                pb.AppendBreak(PromptBreak.Small);
            }

Die Länge der Pausen haben wir relativ angegeben, es bleibt Hedda überlassen, was sie unter Small im Gegensatz etwa zu ExtraLarge versteht. Etwas mehr als eine halbe Sekunde pro Zeile braucht Hedda auf meinem Computer nun zusätzlich, um ans Ende ihres Vortrags zu gelangen, und für meine Ohren sind das (insgesamt) sehr wohltuende 27 Sekunden:

Es fällt nun leichter, die Stellen auszumachen, an denen ihr Vortrag unseren Ansprüchen noch nicht entspricht. Zwei Phänomene fallen besonders auf:

  • Für ein merkwürdiges Zischeln sorgt Heddas Aussprache eines ch (IPA: [x], UPS: [X]) nach den hellen Vokalen zum Beispiel in ich oder mich.
  • Ein z nach Vokalen, beispielsweise in daz, spricht Hedda wie in Batzen. Die Aussprache als tz (IPA: [t.s], UPS: [T + S])  sollte für den Buchstaben z im Mittelhochdeutschen aber eigentlich nur nach Konsonanten oder am Anfang eines Wortes (ze) verwendet werden. Nach Vokalen wird das z  nicht als tz gesprochen, sondern als stimmloses s (IPA: [z], UPS: [Z]).

Das Zischeln bei Heddas Aussprache eines ch nach den hellen Vokalen, scheint darauf zurückzuführen, dass sie nun mal für die Aussprache des modernen Hochdeutschen vorgesehen ist. Selbst ausgebildete Mediävisten, sofern sie mit der neuhochdeutschen Muttersprache aufgewachsen sind, müssen sich sehr konzentrieren, um den hellen Vokalen den weiter hinten am Gaumen gebildeten Laut folgen zu lassen. Wir können Heddas Akzent akzeptieren, wenn wir an sie nicht den Anspruch stellen, eine bessere Aussprache als die meisten Experten zustande zu bringen. Dann aber können wir für die beiden uns auffällig gewordenen Probleme ihrer Aussprache dieselbe Ursache feststellen: Beide Male müsste sie den Laut vor der problematischen Stelle berücksichtigen, um Fehler zu vermeiden.

Um zu erfahren, ob vor einem z ein Vokal in dem von Hedda gelesenen Wort vorkommt, können wir zum Beispiel fragen, ob dieser Buchstabe in der Zeichenkette „aouâôûiîeêöüäœæ“ enthalten ist. Diese enthält alle Buchstaben, die zur Wiedergabe von Vokalen verwendet werden. Darunter sind natürlich auch die, die in Kombination einen Diphthong ergeben. Streichen wir aus dieser Zeichenkette der Vokale diejenigen, die wir als dunkel einstufen, bleiben selbstverständlich nur die zurück, nach denen Hedda von uns aus ein UPS [C] sprechen darf. Wie ist es jedoch hier mit den Diphthongen?

Wir dürfen nicht vergessen, dass es diese Unterscheidung im Mittelhochdeutschen ja gar nicht gibt und wir nur notgedrungen ein neuhochdeutsches Phänomen dulden. Dort aber gilt, dass Diphthonge, die in einem hellen Vokal enden, auch die „helle“ Aussprache des chs herbeiführen. Wir können also die reduzierte Zeichenkette zur Abfrage prinzipiell verwenden. Mit ihr erfassen wir jedoch nicht die Buchstabenkombination iu, die ja keinen Diphthong, sondern ein langes ü wiedergibt. Und dieses ist ebenfalls als heller Vokal zu betrachten. Eine Änderung in FindeAussprache können wir dann so formulieren:

                   if (Wort.Length-i >= Buchstabenfolge.Length && Wort.Substring(i, Buchstabenfolge.Length) == Buchstabenfolge)
                    {
                        string Ersatz = LexionDerBuchstabenErsetzung[Buchstabenfolge];
                        if (Ersatz=="X")
                        {
                            if (i - 1 >= 0 && "iîeêöüäœæ".Contains(Wort[i - 1])) Ersatz = "C";
                            if (i - 2 >= 0 && Wort.Substring(i-2,2)=="iu") Ersatz = "C";
                        }

                        if (Ersatz=="T + S")
                        {
                            if (i - 1 >= 0 && "aouâôûiîeêöüäœæ".Contains(Wort[i - 1])) Ersatz = "Z"; 
                        }
                        Aussprache += Ersatz; // X += Y ist X = X + Y
                        Aussprache += ' '; //nicht das Leerzeichen vergessen!
                        i +=  Buchstabenfolge.Length;
                        Gefunden = true;
                        break; // Ein Ersatz gefunden, wir suchen nicht mehr weiter!
                    }

Wenn wir eine Buchstabenfolge oder einen Buchstaben in einem Wort identifiziert haben, zu der ein UPS-Code im LexikonDerBuchstabenErsetzung existiert, speichern wir den UPS-Code jetzt in der Zeichenkette Ersatz. Falls diese einen Wert enthält, für den wir den Kontext (= die Buchstaben davor) überprüfen müssen, tun wir das und ersetzen gegebenenfalls den Inhalt von Ersatz, bevor wir Ersatz an die Zeichenkette Aussprache anhängen.

Wir nutzen bei unserem Vorgehen erneut, dass nicht nur UPS oder IPA Lautschriften sind, sondern auch das normierte Verschriftungssystem des Mittelhochdeutschen. So können wir einen Zeichenkette aus mittelhochdeutschen Buchstaben bilden, von denen jeder einzelne ein Vokalzeichen repräsentiert, das wir abfragen können. In UPS werden für die meisten von uns abgefragten Vokale hingegen mehrere Buchstaben verwendet. Wir müssten uns also ein anderes Verfahren einfallen lassen, das vermutlich mehr Zeilen Programmcode benötigen würde. Umgekehrt haben wir in Ersatz ein Zeichen oder eine Zeichenkette in UPS-Kodierung zur Verfügung. Das nutzen wir bei der Abfrage des Lautes, der in UPS durch ein einziges [X], im mittelhochdeutschen jedoch durch zwei Zeichen ch wiedergeben wird.

Doch diese Sicht beinhaltet (noch) einen Fehler! Denn das Mittelhochdeutsche verwendet nicht nur ein ch um den UPS-Wert [X] abzubilden! Gleich im zweiten Wort unseres Textes, rehtiu, wird dieser Laut durch ein einfaches h wiedergegeben. Wir haben dieses Wort bereits früher für unsere Experimente genutzt und seit damals ist es in unserem Lexikon mitsamt der von uns ermittelten Lautung enthalten. Denjenigen, denen zu diesem Zeitpunkt bereits aufgefallen ist, dass diese Lautung eigentlich (das neuhochdeutsche ch!) nicht ganz korrekt ist, darf jetzt gratuliert werden. Nicht nur in rehtiu sondern ganz allgemein gilt, dass die Abfolge ht (Weiteres Beispiel: gevohten) stets als cht zu lesen ist, beziehungsweise je nach vorangehendem Vokal neuhochdeutsch als (in UPS-Kodierung) [X T] oder [C T]. Genauso verhält es sich bei hs (Beispiel: wehsel). Auch die Buchstabenfolgen lh (welhes) und rh (vorhten) sind als lch und rch zu lesen. Für sie gelten im Neuhochdeutschen stets UPS: [R C] und [L C]. Wir können unser LexikonDerBuchstabenErsetzung entsprechend ergänzen (die letzte Zeile einer Lexikondefinition darf nicht mit einem Komma abgeschlossen werden!):

            ["zz"]="Z", // hier ein Komma eintragen!!
            // ht,lh,rh
            ["ht"]="X T",
            ["hs"]="X T",
            ["lh"]="L C",
            ["rh"]= "R C"

Damit auch niht und wehsel ohne ein Zischen zu vernehmen sind, ändern wir eine Zeile in FindeAussprache ab.

Aus:

if (Ersatz == "X")

machen wir:

if (Ersatz[0]=='X')

Wer will kann das Lexikon übrigens noch um folgende Werte ergänzen, die Hedda ein r nach Vokalen „hochdeutscher“ aussprechen lässt:

            // deutsche r-Laute:
            ["ür"]="UYX",
            ["or"]="OWX",
            ["ör"]="OE + AX",
            ["oer"]="EU lng + AX",
            ["œr"] = "EU lng + AX",
            ["er"]="EHX",
            ["ûr"]="UAX",
            ["aer"] = "EH lng + AX",
            ["ær"] = "EH lng + AX",

Und dann hören wir uns Heddas verbesserten Vortrag an:

Wir sind jetzt beinahe zufrieden, aber bevor wir aus Heddas Lesung ein Hörbuch machen, wollen wir noch ein paar  weitere „Schönheitkorrekturen“ vornehmen, die vorrangig die Betonung betreffen.

… im sechsten Teil unseres Projektes.

 

Mein erstes mittelhochdeutsches Hörbuch – Teil 4

Dies ist der vierteTeil des Tutorials. Der Beginn findet sich hier.

Von Lautschrift zu Lautschrift

Zumindest die Mediävisten unter uns sollte es kaum erstaunen (und diese dürfen deshalb auch die nächsten Absätze bis hierhin überspringen), wenn sich die in unserem Text verwendeten Graphien 1 : 1 in eine Lautschrift übertragen lassen. Denn es handelt sich ja bereits um eine solche, ein System zur Transkription gesprochener Sprache!

Die Rechtschreibung moderner Sprachen steht oft in einem verwirrenden Verhältnis zur Lautung. Diese Verwirrung ergibt sich schon beinahe zwangsläufig aus dem Konzept der Rechtschreibung selber: Die Regelung der Schreibweisen soll verbindlich und allgemeingültig sein und sich durch eine innere Logik auszeichnen, die es leicht macht, sie zu erlernen und im Gedächtnis zu behalten.

  • Das Gebot der Verbindlichkeit führt jedoch häufig dazu, dass an Schreibungen auch dann noch festgehalten wird, wenn sich die Aussprache ändert.
  • Das Gebot der inneren Logik hingegen kann in den Widerspruch zur Lautung geraten, wenn beispielsweise an der Schreibung eines deutschen Wortes auch ablesbar sein soll, zu welchem Paradigma es gehört (vgl. Rad [r a: t] – Räder).

Es wäre ein wenig überheblich, würden wir es nun so sehen wollen, als ob die mittelhochdeutsche Schreibung geringere Ansprüche an sich selber gestellt habe. Es waren andere Ansprüche. Als Mönche im Mittelalter begannen, volkssprachliche Texte aufzuzeichnen, sahen sie ihre Aufgabe primär darin, die barbarischen Laute in den lateinischen Buchstabenzeichen so wiederzugeben, das Leser in der Lage waren, sie beim Lesen wieder in gesprochene Sprache zu verwandeln. Manchmal mussten die Schreiber dabei kreativ werden, um Töne zu verschriftlichen, die dem Lateinischen unbekannt gewesen waren. Und schon deshalb entstanden irgendwann, als die volkssprachlichen Texte häufiger wurden, auch im Mittelalter Normierungen und Konventionen, die zu nicht immer unmittelbar auf der Basis des Lateins nachvollziehbaren Schreibungen führten.

Im 19. Jahrhundert versuchten Philologen das Gestrüpp zu entwirren und entwickelten aus den Gebräuchen der mittelalterlichen Schreibstuben eine eigene, künstliche Orthographie, die sie ihren Editionen zugrundelegten. So konnten sie offensichtliche Verschreibungen korrigieren, Abkürzungen auflösen und von individuellen Marotten oder lokalen Gepflogenheiten abstrahieren. Man muss bedenken, dass viele Texte nur in sehr viel späteren Abschriften bekannt sind, und dass die Gelehrten des 19. Jahrhunderts ihre Aufgabe zuallererst darin sahen, den ursprünglichen Urtext zu rekonstruieren. Sie legten also besonderen Wert gerade auf die exakte Ermittlung und Wiedergabe der Lautung, so wie sie vom Autor (ein dem 19. Jahrhundert fast schon heiliger Begriff!) einmal gemeint war.

Der Text, den wir Hedda zum Vorlesen vorsetzen, entstammt einer solchen Edition (der Digitalisierung einer solchen Edition) und selbstverständlich ist es deshalb möglich, die dort anzutreffende Schreibung in eine (andere) Lautschrift umzusetzen. Wir müssen also einfach eine Methode FindeAussprache in Hedda.cs einfügen in der wir nach und nach die mittelhochdeutschen Graphien durch Lautzeichen in der UPS-Notation ersetzen, um ein Wort unseres Textes für Hedda aufzubereiten:

        public string FindeAussprache(string Wort)
        {
            string Aussprache = Wort;
            Aussprache = Aussprache.Replace("î", "I lng");
            Aussprache = Aussprache.Replace("iu", "Y lng");
            Aussprache = Aussprache.Replace("ei", "E lng + IH");
            Aussprache = Aussprache.Replace("ie", "I lng + EH");

            /* ...
            * Weitere Ersetzungen folgen ...
            * ... bis am Ende jede mittelhochdeutsche Graphie
            * durch ein UPS-Lautzeichen ersetzt ist
            * und Aussprache von Hedda verarbeitet werden kann.
            */

            return Aussprache;
        }

Wir erzeugen eine Kopie des Ausgangswort, die wir Aussprache nennen. Dann ersetzen wir in mehreren Anläufen Aussprache immer wieder durch eine Kopie von sich selber, in der wir alle Vorkommen jeweils eines mittelhochdeutschen Buchstaben (einer Buchstabenfolge) durch die entsprechende UPS-Notation ersetzt haben. Falls das mittelhochdeutsche Ausgangswort den zu ersetzenden Buchstaben nicht enthielt, bedeutet dies, dass die neue Kopie von Aussprache identisch mit der alten ist. Wenden wir die bisher kodierten Ersetzungsbefehle nicht auf ein einzelnes Wort, sondern zum Beispiel auf die ganze zweite Zeile des Lanzelet an, dann können wir uns diese Austauschschritte etwas besser vorstellen:

der gedenke wie ein wîse man
der gedenke wie ein w I lngse man
der gedenke wie E lng + IHn w I lngse man
der gedenke wI lng + EH E lng + IHn wI lngse man

… usw. …

Doch halt! Am jeweiligen Ende der ersten vier Ersatzbefehlen haben wir das in der UPS-Notation vorgeschriebene Leerzeichen vergessen, das Hedda braucht, um die Segmente der Lautschrift zu separieren. Das müssen wir per Hand nachtragen und dürfen es in Zukunft nicht vergessen. Gut, dass uns das jetzt aufgefallen ist. Man stelle sich vor, wir hätten bereits mühsam alle unsere Ersetzungsbefehle kodiert!

Tatsächlich müssen wir jedoch damit rechnen, dass sich noch allerlei Änderungen und Korrekturen ergeben, sobald wir erste Tests mit Heddas verbesserter Aussprache des Mittelhochdeutschen anstellen. Niemand ist schließlich perfekt. Es muss aber auch gar nicht unbedingt an unserer schlechten Planung und phonetischen Vorstellungsgabe liegen, wenn Heddas Vortrag der UPS-Zeichen nicht ganz unseren Erwartungen entspricht. Wir machen uns ja erst vertraut mit der Technologie. Auch deshalb werden wir den schon einmal nützlichen Kniff wiederverwenden und uns ein weiteres Lexikon anlegen. Diesmal werden wir darin die zu ersetzenden Buchstaben und die entsprechenden UPS-Zeichen aufnehmen. So verschaffen wir uns einen besseren Überblick, reduzieren den Schreibaufwand und sind für die späteren Korrekturen gerüstet. Dann können wir FindeAussprache entsprechend abändern:

        Dictionary<string, string> LexionDerBuchstabenErsetzung = new Dictionary<string, string>
        {
            ["î"] = "I lng",
            ["iu"] = "Y lng",
            ["ei"] = "E lng + IH",
            ["ie"] = "I lng + EH"
            // Weitere Ersetzungen folgen ...
        };
        public string FindeAussprache(string Wort)
        {
            string Aussprache = Wort;
             foreach(string Grapheme in LexionDerBuchstabenErsetzung.Keys)
            {
                /* Wir gehen das Lexikon durch und
                 * ersetzen die im Ausgangswort vorgefundenen Buchstaben
                 * durch die diesen entsprechenden Einträge.
                 * Am Ende jeder Ersetzung ergänzen wir ein Leerzeichen.
                 */
                Aussprache = Aussprache.Replace(Grapheme, LexionDerBuchstabenErsetzung[Grapheme]+' ');
            }            return Aussprache;
        } 

Durch das neue LexikonDerBuchstabenersetzung haben wir uns wohl zukünftig Tipparbeit gespart. Bis wir es gefüllt haben, werden wir trotzdem noch ganz gut zu tun haben. Und einige Ersetzungen sind ziemlich redundant, unser Lexikon müsste beispielsweise auch diesen Eintrag beinhalten:

         ["t"] = "T",

oder diesen:

         ["f"] = "F",

Jeglicher Buchstabe, „der gesprochen wird, wie man ihn schreibt“, bei dem die UPS-Notation und das System zur Wiedergabe mittelhochdeutscher Laute das gleiche lateinische Buchstabenzeichen verwenden, müsste so aufgeführt werden. Selbstverständlich kann man eine solche Aufgaben automatisieren. Und selbstverständlich werden wir das tun! Eine kleine Programmschleife wird noch vor der ersten Verwendung des Lexikons die entsprechenden Befüllungen vornehmen. Wir ergänzen unsere Klasse Hedda.cs um einen Konstruktor. So wird eine Methode genannt, die bei jeder Erzeugung einer Instanz von Hedda genau einmal aufgerufen wird und Initialisierungen vornimmt. Der Konstruktor hat stets denselben Namen, wie die Klasse selber. Hedda.Hedda füllt also unser Lexikon mit den gewünschten Einträgen:

        public Hedda()
        {
            string EinfachBuchstaben = "bdfghklmnprst";
            foreach(  char c in EinfachBuchstaben)
            {
                LexionDerBuchstabenErsetzung[c.ToString()] = c.ToString().ToUpper();
            }
        }

UPS verwendet Großbuchstaben zur Kodierung. Ersetzt werden müssen nur die Kleinbuchstaben, die UPS nicht kennt. Die haben wir in einer Zeichenkette zusammengestellt und ordnen jetzt einfach jedem kleingeschriebenen Einzelbuchstaben von EinfachBuchstaben einen Eintrag zu, der denselben Buchstaben, jetzt aber großgeschrieben, enthält. Die Liste der Buchstaben ist vielleicht noch nicht ganz vollständig (wo sind beispielsweise die Vokale?), aber wir können Lücken ja nach Bedarf schließen. Und warum übernehmen wir diese praktische Methode nicht gleich ganz zur Füllung unseres Lexikons? Beispielsweise auf diese Weise:

            string KomplexereErsetzungen = "î-I lng,iu-Y lng,ei-E lng + IH,ie-I lng + EH";
            foreach(string Ersatzpaar in KomplexereErsetzungen.Split(','))
            {
                string[] Ersatz = Ersatzpaar.Split('-');
                LexionDerBuchstabenErsetzung[Ersatz[0]] = Ersatz[1];
            }

Diesmal haben wir eine Zeichenkette erzeugt, in der wir die Lexikoneinträge durch Kommata voneinander abtrennen. Jeder Eintrag besteht wiederum aus zwei Teilen: Der mittelhochdeutschen Graphie auf der einen Seite des Bindestrichs und der UPS-Lautschrift auf der anderen. Jetzt müssen wir diese Kodierung nur wieder durch intelligente Verwendung von Split() aufspalten und können unser Lexikon so befüllen. Das erste, vom Rest durch ein Komma getrennte, Paar eines mittelhochdeutschen Buchstaben und der ihm zugeordneten UPS-Zeichen ist zum Beispiel î-I lng. Das Lexikon erhält nach dem Entpacken in der span style="font-family: courier new,courier,monospace;">foreach-Schleife den neuen Eintrag:

    ["î"] = "I lng";

Dieser Eintrag sollte bekannt vorkommen. Wer aufmerksam liest, erkennt, dass es sich bei den Ersatzpaaren in der durch Kommata getrennten Liste bisher um dieselben Einträge handelt, die wir zuvor etwas konventioneller in unser Lexikon eingetragen hatten. Man muss aber wirklich aufmerksam hinsehen, denn was wir mit dieser Methode an Tipparbeit sparen, verlieren wir an Übersichtlichkeit.

Egal auf welche Weise wir unser Lexikon der Buchstabenersetzungen auch befüllen, es gibt drei Schwierigkeiten, mit denen wir uns auseinandersetzen müssen:

  1. Wie garantieren wir die Vollständigkeit der Ersetzungen?
  2. Wie gehen wir mit mehrdeutigen Buchstabenfolgen um?
  3. In welcher Abfolge ersetzen wir?

Das erste Problem ist durch Heddas Verhalten bei unkorrekten UPS-Notationen bedingt: Wenn sie nicht jedes Lautsegment der Abfolge einer im SSML-Format kodierten Zeichenkette identifizieren kann, bricht sie ihren Versuch der Interpretation ab. Wir müssen ihr also einen Text zum Sprechen vorlegen, der nur solche Zeichen enthält, die sie umsetzen kann. Das ist gar nicht so einfach, wie es erscheinen mag. Unser kleiner Textausschnitt ist relativ leicht überschaubar. Aber was ist, wenn wir Hedda das Nibelungenlied zum Lesen präsentieren? In einem Format, möglicherweise, das auch editorische Randbemerkungen enthält, in denen Buchstaben vorkommen, die nicht zum typischen Alphabet des Mittelhochdeutschen gehören? Beispielsweise könnte die Originalschreibweise eine französischen, griechischen oder italienischen Namens angegeben werden, ein linguistischer oder literaturwissenschaftlicher Fachterminus erscheinen … Wenn wir darauf hoffen sollten, Hedda auch einen Text zum Vorlesen vorlegen zu können, den wir nicht Zeile für Zeile für sie präpariert haben (… und könnten wir dann nicht einfacher selber lesen?), wird  sie zwangsläufig irgendwann auf Buchstabenzeichen stoßen, für die wir ihr keine Ausspracheregelung vorgegeben haben.

Mehrdeutige Buchstaben und Buchstabenfolgen ergeben sich aus zwei Gründen. Zum Einen kodiert ein Zeichen manchmal mehrere Aussprachevarianten, von denen aber im jeweiligen Kontext nur eine realisiert wird. Im modernen Hochdeutschen beispielsweise verwenden wir die Buchstabenfolge ch sowohl zur Bezeichnung eines sehr weit hinten am Gaumen, fast schon im Rachen produzierten Lautes ( Beispiel: lachen IPA: [x]), als auch eines viel weiter vorne am Gaumen entstehenden Zischlautes (Beispiel: Kichern IPA: [ç]). Beide Varianten des Bigraphen werden von deutschen Muttersprachlern beim Sprechen nicht durcheinandergebracht, denn [x] tritt nur hinter den „dunklen“ Vokalen a/o/u auf, [ç] hinter den „hellen“ e/i/ä/ö/ü. Das führt dann dazu, dass sich diese Stelle in Wortpaaren wie der Fluch – die Flüche, die Rache – rächen, das Loch – die Löcher bei genauem Hinhören unterscheidet. Einige deutsche Dialekte haben übrigens die Unterscheidung ein wenig weiter getrieben: Bei ihnen rutscht das ch nach dunklen Vokalen noch tiefer in den Rachen und aus dem ch nach hellen Vokalen wird ein sch.

Doch für ein Programm, das Ersetzungen nach den Einträgen eines Lexikons vornimmt, gibt es noch eine zweite Form der Mehrdeutigkeit, sobald ein ch in seinem Ausgangstext erscheint. Schließlich beinhaltet sein Lexikon auch eine Vorschrift, was es tun soll, wenn es auf ein einzelnes c oder ein einzelnes h trifft, die ja ebenfalls in einem hochdeutschen Wort vorkommen können. Welche Regel ist nun anzuwenden?  Unser Lexikon der mittelhochdeutschen Graphien enthält bisher bereits drei Einträge für Buchstabenfolgen, für eine Abfolge mehrerer Buchstaben, die zusammen gelesen und gemeinsam durch eine UPS-Notation ersetzt werden sollen. Bei ei und ie handelt es sich (mittelhochdeutsch) um Diphthonge, also Doppellaute. Die Notation durch zwei Buchstabenzeichen bietet sich durchaus an. Es macht einen wahrnehmbaren Unterschied, ob Hedda an diesen Stellen UPS-konform EH I oder E lng + IH spricht! In einer mittelhochdeutschen Wortform wie ge-irren wäre der Diphthong ei allerdings fehl am Platz. Und iu ist gar kein Doppellaut, sondern wird als langes ü ( IPA: [y:], UPS: [Y lng]) gesprochen!

Spätestens jetzt müssen wir auch darüber nachdenken, in welcher Reihenfolge die Ersetzungen vorgenommen werden sollen. Denn Wörter wie meien, leien, reien etc. sind gar nicht so selten in mittelhochdeutschen Texten. Wird auf diese Wörter als Erstes die Ausspracheregel [„ie“] = „I lng + EX“ angewendet, dann ergibt sich als Ergebnis dieses Austauschs „meI lng + EX n„. Wenn Hedda alle Ersetzungen vorgenommen hat, sollten wir danach Folgendes hören:

Doch selbst dieses Ergebnis dürfen wir nicht unbedingt erwarten, denn die Zeichenfolge lng besteht aus l n und g. Und das sind ebenfalls mittelhochdeutsche Buchstaben, die unser Programm sonst durch die UPS-konformen L, N und G ersetzen soll, so dass das Ergebnis unserer Interpretation von meien auch M E I L N G + EX N oder M E L N G + IH EH N lauten könnte:

Es verschiedene Möglichkeiten mit diesen Problemen umzugehen. Bleiben wir allerdings dabei, unsere Ersetzungen durch eine Abfolge von Replace-Befehlen durchzuführen, wird es sehr schwierig, die Kontrolle über die Ersetzungen zu behalten und eine Abfolge auszuknobeln, die zum gewünschten Ergebnis führt. Wir sollten es nicht als Kapitulation vor diesen Schwierigkeiten auffassen, wenn wir eine andere Lösung wählen, und Hedda sich von nun an brav wie eine Erstklässlerin Buchstabe für Buchstabe durch den Text arbeiten lassen. Trifft sie dabei auf ein e, wird sie solange mit der Entscheidung, wie sie dieses e aussprechen soll warten, bis sie sich auch den nächsten Buchstaben angesehen hat. Handelt es sich bei diesem beispielsweise um ein i, dann gehören die beiden Buchstaben wohl zusammen und müssen als ei ausgesprochen werden. Wenn Hedda irgendwann einmal einen ihr völlig unbekannten Buchstaben vorfindet, überspringt sie den (Wir kümmern uns später darum, versprochen!) und liest einfach weiter. Hedda frisst sich durch den mittelhochdeutschen Text und hinterlässt hinter sich ein Spur „verarbeiteter“ lautsprachlicher Zeichen, die sie uns dann später (lesend) vorsetzt:

S V E lng R R E C T Y lng 

Das ist eine Metapher, über die wir einen Moment nachdenken müssen, bevor wir uns daran machen, sie als Programmroutine zu implementieren.

… im fünften Teil des Projektes

 

Mein erstes mittelhochdeutsches Hörbuch – Teil 3

Dies ist der dritte Teil des Tutorials. Der Beginn findet sich hier.

Der Blick ins Lexikon hilft!

Wir wollen, dass Hedda uns einen mittelhochdeutschen Text flüssig vorliest und das in möglichst korrekter Aussprache. Um beiden Anforderungen gerecht zu werden,  blieb uns nichts anderes übrig, als den gesamten Text in Lautschrift zu übersetzen und dann im Ganzen an Hedda zur Aussprache zu übergeben. Denn als wir versuchten, den Text in Einzelpassagen zu zerlegen, die wir Hedda in unterschiedlichem Format übergaben, war die Unterbrechung und der Neuansatz bei jeder Einheit deutlich und störend zu hören. Dabei könnte es doch so einfach sein: Hedda ist für die Ausgabe moderner deutscher Texte optimiert und bei diesen hat sie keine Schwierigkeiten, sie relativ unfallfrei und sogar mit so etwas wie einer angemessenen Betonung in flüssigem Vortrag vorlesen. Trifft sie dabei auf ihr unbekannte Wörter, liest sie ohne Unterbrechung so, wie es der üblichen Aussprache deutscher Buchstaben entspricht. Da sich jedoch das Mittelhochdeutsche nicht in jeder Hinsicht vom modernen Hochdeutsch unterscheidet, könnten wir häufig ihre Voreinstellungen übernehmen und müssten nur in Einzelfällen eingreifen.  Wir bräuchten dafür nur eine Möglichkeit, Hedda einen Text im Ganzen zum Lesen vorzulegen, in dem Passagen in Lautschrift auf solche in Buchstabenschrift folgen und umgekehrt.

Tatsächlich ist das möglich. Als Prompt wird in der englischsprachigen Terminologie der speech.dll eine sprachliche Äußerung verstanden und mit der Klasse Promptbuilder gibt es die Möglichkeit, eine solche Äußerung aus elementaren und heterogenen Bestandteilen zusammenzusetzen. Probieren wir es gleich aus:

        static void Main(string[] args)
        {
            
            SpeechSynthesizer synth = new SpeechSynthesizer();
            
            PromptBuilder pb = new PromptBuilder();
            pb.AppendText("Swêr");
            pb.AppendSsmlMarkup(@"<phoneme alphabet=""ups"" ph=""S1 R EH C . T Y lng"">rehtiu</phoneme>");
            pb.AppendText("wort gemerken kan,");

            synth.Speak(pb);
          
        }

 

 

Heureka! Mit dieser Technik können wir uns viel Arbeit ersparen. Eine zusätzliche Ersparnis ergibt sich durch die Verwendung eines Lexikons. In diesem vermerken wir die Sonderfälle der Aussprache für Hedda. Diese müssen wir dann nur einmal angeben und nicht bei jedem Vorkommen im Text. Wir müssen Hedda nur mitteilen, dass sie von nun an für jedes Wort im Text überprüfen soll, ob es zu diesem einen Eintrag im Lexikon der Sonderfälle gibt:

 

        static void Main(string[] args)
        {

            SpeechSynthesizer synth = new SpeechSynthesizer(); 
            PromptBuilder pb = new PromptBuilder();

            // Ein Lexion zur Aufnahme der abweichend auszusprechenden Wortformen:
            Dictionary<string, string> Lexikon = new Dictionary<string, string>
            {
                ["rehtiu"] = "S1 R EH C . T Y lng"
            };

            // unser Beispieltext:
            string Text = "Swêr rehtiu wort gemerken kan,";

            /* Los geht's Hedda!
             * Lies Wort für Wort.
             * Überprüfe, ob das Wort im Lexikon vermerkt wurde.
             * Wenn ja, dann folge der dort in Lautschrift notierten Aussprache.
             * Wenn nicht ... mach's wie immer, sprich so, wie es da "geschrieben" steht.*/

            foreach (string Wort in Text.Split(' '))
            {
                if (Lexikon.Keys.Contains(Wort))
                {
                    //Sonderfall:
                    pb.AppendSsmlMarkup(@"<phoneme alphabet=""ups"" ph=""" +Lexikon[Wort]+@""">"// Die Lautschriftvariante
                                        +Wort+"</phoneme>");//  gefolgt von der Graphie
                }
                else
                {
                    pb.AppendText(Wort); // Normalfall: Hochdeutsche Aussprache.
                }
            }       

            synth.Speak(pb);  
        }
    }

 

An Heddas Vortrag hat sich nichts geändert, nur an der Art und Weise, wie er zustande kam. Deshalb sollen die Details noch einmal genauer betrachtet werden. Wir haben es diesmal Hedda, bzw. dem von uns geschriebenen Computerprogramm überlassen, den vorzulesenden Text in Wörter zu zerlegen. Wir tun das durch die Anweisung Split(' '), die der alten computerlinguistischen Faustregel folgt, dass als Wort jede Abfolge von Zeichen zwischen zwei Leerzeichen angesehen wird. Das ist selbstverständlich eine grobe Vereinfachung, die aber für unsere Zwecke erst einmal genügt. Wort für Wort schlägt das Programm dann unter den Schlüsselbegriffen mit Lexikon.Keys.Contains(Wort) im Lexikon nach und hängt gegebenenfalls den dort vorgefundenen Eintrag an das Prompt der Aussage an. Gibt es keinen solchen Eintrag, dann wird das Wort selber an Hedda zur späteren Verarbeitung weitergereicht.

Nun ist unser Lexikon mit exakt einem Eintrag nicht gerade umfangreich. Aber wir haben uns ja auch nicht die Aufgabe gestellt, nur eine einzige Verszeile in gesprochene Sprache umzuwandeln, sondern wollten uns ein ganzes Buch vorlesen lassen. Oder zumindest die ersten 50 Verse davon. Doch bevor wir daran gehen, werden wir ein wenig „aufräumen“.

Für unsere bisherigen Experimente haben wir nur ein paar Zeilen Programmiercode gebraucht, die wir in der statischen Methode Main untergebracht haben. Wir wollen jetzt aber zu komplexeren Arbeitsschritten übergehen, die Hedda erledigen soll. Und deshalb werden wir nun als nächstes eine eigene Klasse für diese Aufgaben anlegen und unserem Projekt hinzufügen. Der geben wir den Namen Hedda.cs. Dann können wir auch weiter, und jetzt mit deutlich mehr Berechtigung davon sprechen, dass wir Hedda einen Befehl geben, oder Hedda etwas im Lexikon nachschlägt:

Die automatisch generierte Klasse Hedda.cs ändern wir ab, so dass sie nun so aussieht:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Speech.Synthesis;

namespace Mittelhochdeutsches_Hörbuch1
{
    class Hedda
    {
        // Ein Lexion zur Aufnahme der abweichend auszusprechenden Wortformen:
        private Dictionary<string, string> Lexikon = new Dictionary<string, string>
        {
            ["rehtiu"] = "S1 R EH C . T Y lng"
        };

        public  void LeseZeile (string Zeile, PromptBuilder pb)
        {

            /* Los geht's Hedda!
             * Lies Wort für Wort.
             * Überprüfe, ob das Wort im Lexikon vermerkt wurde.
             * Wenn ja, dann folge der dort in Lautschrift notierten Aussprache.
             * Wenn nicht ... mach's wie immer, sprich so, wie es da "geschrieben" steht.*/

            foreach (string Wort in Zeile.Split(' '))
            {
                if (Lexikon.Keys.Contains(Wort))
                {
                    //Sonderfall:
                    pb.AppendSsmlMarkup(@"<phoneme alphabet=""ups"" ph=""" + Lexikon[Wort] + @""">"// Die Lautschriftvariante
                                        + Wort + "</phoneme>");//  gefolgt von der Graphie
                }
                else
                {
                    pb.AppendText(Wort); // Normalfall: Hochdeutsche Aussprache.
                }
            }
        }
    }
}

 

Problemlos dürften sich die Elemente wiedererkennen lassen, die wir aus der Methode Main in Program.cs übernommen haben: Das Lexikon, die Schleife, in der wir Hedda die Worte der Zeile lesen lassen … Nur, dass wir diese jetzt in eine Methode der Klasse Hedda.cs, einen Befehl an Hedda, verpackt haben. Wichtig ist es übrigens, nicht die Änderung an den using – Anweisungen zu übersehen: Damit Hedda ihren Dienst tun kann, braucht auch sie die Speech.dll. „Unsere“ Methode Main müssen wir auch noch ändern, sie sieht jetzt so aus:

        static void Main(string[] args)
        {


            SpeechSynthesizer synth = new SpeechSynthesizer(); 
            PromptBuilder pb = new PromptBuilder();
            Hedda Hedda = new Hedda();

            //Unsere Textdatei: 
             
            StreamReader Textdatei = new StreamReader("D:/Lanzelet 1 - 49.txt");

            string Zeile;

            /* Schleife:
             * Solange bis das Ende der Textdatei erreicht ist (der Befehl ReadLine() null ergibt)
             * Lies die nächste Zeile ein
             * Und übergib sie an Hedda zum Vorlesen.
             */
            while(( Zeile = Textdatei.ReadLine())!=null)
            {
                Hedda.LeseZeile(Zeile, pb);
            }
            
            synth.Speak(pb);  
        }
[/csharp]

&nbsp;

<em><strong><span style="color: #ff0000;">ACHTUNG!</span></strong> Ich habe die Textdatei, die wir verwenden, auf meinem Rechner im Hauptverzeichnis von Laufwerk D: abgespeichert. Wenn sie auf einem anderen Laufwerk oder Verzeichnis gespeichert wurde, muss die Pfadangabe natürlich entsprechend angepasst werden.</em>

Und auch in <code><span style="font-family: courier new,courier,monospace;">Program.cs</span></code> müssen wir noch eine <code><span style="font-family: courier new,courier,monospace;">using</span></code>-Zeile einfügen, damit <code><span style="font-family: courier new,courier,monospace;">Main</span></code> die Datei einlesen kann:

[code language="csharp"]using System.IO;

Dann können wir uns entspannt zurücklehnen und unser erstes Hörbuch genießen …

Na?

Zufrieden?

Wie …? Wirklich?    … gar nicht?

Es ist wohl nicht zu leugnen: Wir haben noch etwas Arbeit vor uns!!!

Wir mögen damit zurechtkommen, wenn Hedda beim Vortrag einer einzelnen Verszeile, deren Text wir am Bildschirm mitlesen können, ein einziges Wort als völlig unverständliche Lautfolge umsetzt. Wenn wir sie jedoch eine längere Passage lesen lassen, ist es kaum noch möglich ihr zu folgen. Wir sollten uns jedoch vom ziemlich katastrophalen ersten Höreindruck nicht enttäuschen lassen. Schließlich enthält unser Lexikon ja bisher nur einen Eintrag! So kann es kaum eine Hilfe darstellen. Füllen wir es doch mit ein paar Einträgen und ändern die Klasse Hedda.cs entsprechend ab:

        // Ein Lexion zur Aufnahme der abweichend auszusprechenden Wortformen:
        private Dictionary<string, string> Lexikon = new Dictionary<string, string>
        {
            ["rehtiu"] = "S1 R EH C . T Y lng",
            ["gemerken"]="G EX . S1 M EH R . K EX N",
            ["gedenke"]="G EX . S1 D EH N . K EX",
            ["wie"] = "S1 V I lng + EX",
            ["ein"] = "E + I N",
            ["wîze"] = "S1 V I lng . Z EX",
            ["hie"] = "S1 H I lng + EX",
            ["bî"] = "S1 B I lng",
            ["zîten"]= "S1 TS I lng . T EX N",
            ["sît"] = "S1 S I lng T",
            ["diu"] = "D Y lng",
            ["dûhte"] = "S1 D U lng X . T EX",
            ["niht"] = "N IH C T",
            ["gemuot"] = "G EH . S1 M U lng + O T"

        };

 

Die ersten fünf Verszeilen sind nach der kleinen Änderung jetzt deutlich besser zu verstehen:

… und wenn wir uns konzentrieren, begegnen uns auch im weiteren Text einige der neuen Einträge unseres Lexikons wieder und machen diesen damit vereinzelt verständlicher.

Versuchen wir es positiv zu sehen: Wir sind auf dem richtigen Weg!

Ein Weg, der allerdings noch recht lang zu werden verspricht, wie diejenigen sicher bestätigen werden, die sich die Mühe gemacht haben, die Lexikoneinträge per Hand in Hedda.cs einzutragen und sie nicht einfach per copy & paste übernommen haben. Diesen fleißigen Tippern wird dabei jedoch vermutlich auch aufgefallen sein, dass die Umsetzung der Grapheme in Phoneme sehr regelhaft erfolgt. Als UPS-Notation der Aussprache für ein î haben wir zum Beispiel in wîze, bî, zîten und sît jeweils I lng ermittelt (IPA-Notierung: [i:]). Nach unserer bisherigen Erfahrung gilt anscheinend:

Bestimmten Buchstaben (-folgen) entsprechen stets und immer wieder bestimmte (Abfolgen von) Lautzeichen.

Wir werden versuchen aus dieser Beobachtung Nutzen zu ziehen, um uns weitere (unnötige) Tipparbeit zu ersparen. Und damit beginnen wir umgehend …

… im vierten Teil unseres Projektes.

Mein erstes mittelhochdeutsches Hörbuch – Teil 2

Dies ist der zweite Teil des Tutorials (daher heißt es auch Teil 2 im Titel). Der erste Teil findet sich hier.

Hedda spricht (eine Zeile) Mittelhochdeutsch!

Wir legen los und in Visual Studio ein neues Projekt an:

Aus dem Auswahlmenü wählen wir die Konsolen-App (Net-Framework), der wir einen sprechenden Namen geben. Wie wäre es mit „Mittelhochdeutsches Hörbuch1“? Unter welchem Namen auch immer, das Projekt wird erzeugt und uns die Datei Program.cs präsentiert:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Mittelhochdeutsches_Hörbuch1
{
    class Program
    {
        static void Main(string[] args)
        {
        }
    }
}

Bevor wir unseren Computer zum Sprechen bringen können, müssen wir noch die zur Spracherzeugung benötigte Bibliothek einbinden. Dazu öffnen wir den Reiter „Projekt/Verweis hinzufügen“.

In den Assemblys aktivieren wir „System.Speech“ mit einem Häkchen:

Dann ändern wir die Ausgangsdatei (per copy & paste oder abtippen) folgendermaßen ab und starten mit F5 unser erstes Experiment:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Speech.Synthesis;

namespace Mittelhochdeutsches_Hörbuch1
{
    class Program
    {
        static void Main(string[] args)
        {            
            SpeechSynthesizer synth = new SpeechSynthesizer();
            synth.Speak("Swêr rehtiu wort gemerken kan,");            }
        }
    }
}

 

Jetzt sollten wir eine Stimme hören, die mit der erwarteten falschen Aussprache unseren Text spricht. Es ist gut möglich, dass diese Stimme uns unbekannt ist und nicht derjenigen entspricht, die wir von der Windows-Sprachausgabe gewohnt sind. Auf meinem Rechner ist es jedenfalls eine weibliche Stimme und damit definitiv nicht Stefan, die ich höre. Der Grund hierfür ist, dass Stefan dafür vorgesehen ist, Deutsch mit mir zu sprechen und anscheinend nur dazu in der Lage ist. Ihm fehlt die Möglichkeit, sich an ein anderes Idiom anzupassen. Etwas technischer formuliert: Das Sprachprofil, das mit dem Namen Stefan verbunden wird, bietet nicht alle Möglichkeiten, die durch die Programmierschnittstelle System.Speech.dll vorgesehen sind, und steht uns damit nicht zur Verfügung. Oder vielmehr: mir, zum Zeitpunkt, an dem ich dies schreibe. Denn selbstverständlich kann sich das mit dem nächsten Windows-Update ändern oder mit der nächsten Generation der Speech-Schnittstelle.

Wir können uns, wenn wir wollen, beim nächsten Durchlauf unseres kurzen Programms die uns zur Verfügung stehenden Stimmen vorstellen lassen, indem wir unsere statische Methode Main leicht verändern:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Speech.Synthesis;

namespace Mittelhochdeutsches_Hörbuch1
{
    class Program
    {
        static void Main(string[] args)
        {
            
            SpeechSynthesizer synth = new SpeechSynthesizer();

            foreach (var voice in synth.GetInstalledVoices())
            {
                synth.SelectVoice(voice.VoiceInfo.Name);
                synth.Speak(voice.VoiceInfo.Description);
                synth.Speak("Swêr rehtiu wort gemerken kan,");            }
            }
        }
     }
}

 

Mir stehen eine weibliche Stimme mit dem Namen Hedda zur Verfügung, die deutsch spricht, und eine Amerikanerin namens Sira. Ob es sich bei der um eine Verwandte von Apples ureigener Siri handelt? Und wer hat der „Deutschen“ den so typisch deutschen Namen Hedda verpasst? Die Auswahl der Stimmen ist sehr begrenzt, aber zumindest durch die Namensgebung für einen kleinen nerdigen Lacher gut.  Wer wie ich mit Hedda leben muss (Nein, Sira kann ich mir nicht als Vorleserin der Aventiure vorstellen), mag vielleicht mit der Option käuflicher Stimmenprofile liebäugeln, aber eigentlich reicht ja eine Stimme für unser Vorhaben.

Sofern wir sie dazu bringen können, phonetisch korrektes Mittelhochdeutsch von sich zu geben.

Hedda sollte also eine Lautschrift verstehen, beziehungsweise in der Lage sein, einen in dieser transkribierten Text angemessen wiederzugeben. Genau das verspricht uns die Speech.dll, wenn sie Hedda unter den installierten – und der Norm gerechten –  Stimmen aufführt. Und genau das werden wir jetzt austesten. Wenn wir es (vorerst) nicht ganz so kritisch sehen wollen, gibt es in dieser ersten Zeile unseres Textes ja nur ein Wort, das wirklich so falsch klingt, dass wir handeln müssen: rehtiu. Die anderen Wörter sind in ihrer hochdeutschen Aussprache nicht perfekt, aber, wenn wir ein paar Augen zudrücken, halbwegs akzeptabel. Konzentrieren wir uns auf rehtiu und versuchen Hedda beizubringen, das etwas besser zu artikulieren:

 

        static void Main(string[] args)
        {
            
            SpeechSynthesizer synth = new SpeechSynthesizer();
            string ssmlRehtiu =  @"<speak version=""1.0"" xml:lang=""de-DE"">"
                                + @<phoneme alphabet=""ups"" ph=""S1 R EH C . T YH lng"">"
                                + "Rehtiu</Phoneme></speak>";
            synth.SpeakSsml(ssmlRehtiu);
        }

 

 

Das klingt doch schon besser, oder? Was wir hier benutzen, ist der SSML-Standard, ein XML-Codierungsschema zur Steuerung von Sprachausgaben (Speech Synthesis Markup Language). Wenn wir eine dafür passendere Formatierung wählen sieht das ungefähr so aus:

   <speak version="1.0" xml:lang="de-DE">
       <phoneme Alphabet="ups" ph="S1 R EH C . T YH lng"&>rehtiu</Phoneme>
   </speak>

 

Das <speak>-Tag klammert eine Sprachausgabe ein. Innerhalb des <phoneme>-Tags finden wir unseren Ausgabetext wieder, so wie wir ihn kennen („kennen“ heißt, in der Buchstabendarstellung). Die Lautschriftfassung findet sich im Attribut ph = „S1 R EH C . T YH lng“. Es handelt sich um das Lautschriftsystem UPS, eine Microsoft-Eigenkreation. Dass wir dieses Kodierungssystem verwenden, müssen wir natürlich auch angeben: alphabet = „ups“. Im <speak>-Tag vermerken wir noch (verpflichtend!) die Version des Sprachstandards auf die wir uns beziehen und die Sprache, an der sich unsere Hedda orientieren soll. „de-DE“ steht selbstverständlich für das in Deutschland gesprochene Deutsch (im Gegensatz zum Schweizerischen beispielsweise).

Soweit so gut. Hedda ist in der Lage Text in einer Lautschrift zu verstehen. Tatsächlich sind es sogar drei Lautschriften, allen voran das internationale phonetische Alphabet (IPA). Dieses besteht bekanntlich in seinem Kern aus den lateinischen Buchstaben, die um einige Graphien ergänzt wurden, die besonders Altphilologen gut vertraut sind und normalerweise in der Sprache, der sie entnommen wurden, Laute repräsentieren, die es im Lateinischen nicht gab. Viele dieser Zeichen finden sich nicht auf der Standardtastatur und nicht im Standard-ASCII-Zeichensatz. Beispielsweise der ⁠ʃ⁠-Laut in hübsch: [‚hyp⁠ʃ⁠]). Aus diesem Grund gibt es die beiden anderen Varianten der Lautkodierung, die Hedda versteht. Für Computerprogramme leicht zu verarbeiten ist die SAPI-ID (SAPI = Speech API), ein System, das jedem der IPA-Zeichen einen Zahlenwert zuweist: hübsch wird als [02C8 0068 028F 0283] erfasst. Uns kommt das weniger entgegen und deshalb haben wir uns für UPS entschieden, um mit Hedda zu kommunizieren.

UPS nutzt nur Zeichen, die auf der Tastatur (und im ASCII-Code) vorkommen. Häufig, aber nicht immer, entsprechen auch hier einzelne Buchstaben den lateinischen Lautwerten (wenn auch eher in der englischen Aussprache). Komplexere oder (vom englisch getönten Latein her betrachtet) ungewöhnlichere Laute werden durch Buchstabenkombinationen (EH AX etc.) wiedergegeben, die jeweils eine Einheit der Lautzeichenkette repräsentieren. Um die Segmente deutlich voneinander zu trennen muss ein Leerzeichen zwischen ihnen eingefügt werden.

ACHTUNG! Hedda, bzw. die Speech.dll, ist in dieser Hinsicht ausgesprochen pingelig! Fehlt das Leerzeichen, und lässt sich kein einzelner Lautwert finden, der einer Buchstabenfolge zugeordnet werden kann, bricht die Sprachausgabe ab!

In unserem Beispiel findet sich unter anderem auch die Kodierung lng. Sie gibt keinen Lautwert wieder, sondern zeigt an, dass der vorangehende Laut lang gesprochen werden soll (IPA verwendet den Doppeltpunkt [:] zur Bezeichnung der Länge). Auch solche Signale müssen durch Leerzeichen von ihren Nachbarn getrennt werden. Die weitere Erläuterung der Symbole der Lautschrift erspare ich uns vorerst. Sie findet sich schließlich ausführlich auf den Supportseiten von Microsoft dokumentiert.

Machen wir erst einmal weiter: Wir lassen Hedda den Beginn der Zeile sprechen und akzeptieren ihr modernes Hochdeutsch als Mittelhochdeutsch, fügen dann das problematische rehtiu in Lautschrift ein, und setzten danach den Text fort – jetzt wieder Neuhochdeutsch-Pseudomittelhochdeutsch:

 

        static void Main(string[] args)
        {
            
            SpeechSynthesizer synth = new SpeechSynthesizer();
            string ssmlRehtiu =  @""
                                + @""
                                + "rehtiu";
            synth.Speak("Swêr");
            synth.SpeakSsml(ssmlRehtiu);
            synth.Speak("wort gemerken kan,");
        }

 

Das
hört sich
jetzt
sehr
abgehackt an. Vielleicht sollten wir doch gleich die ganze Zeile in phonetischer Schreibweise erfassen. Dann brauchen wir auch nicht so viele faule Kompromisse einzugehen:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Speech.Synthesis;

namespace Mittelhochdeutsches_Hörbuch1
{
    class Program
    {
        static void Main(string[] args)
        {
            
            SpeechSynthesizer synth = new SpeechSynthesizer();

            foreach (var voice in synth.GetInstalledVoices())
            {
                synth.Speak(voice.VoiceInfo.Description);
                string phoneme = " S1 S V EH lng R . "
                                +"S1 R EH C . T YH lng . "
                                +"S1 V AOX T . "
                                +"G EH . S1 M EX R . K EX N . "
                                +"S1 K A N _,";

                string grapheme = "Swêr rehtiu wort gemerken kan,";
                synth.SpeakSsml(@""
                                +grapheme+"");
            }
        }
    }
}

Wenn wir also unseren Text einfach Zeile für Zeile in Lautschrift umsetzen und diese dann zu einer sehr, sehr, seeeehr lange Zeichenkette zusammensetzen … die wir dann natürlich noch zwischen die SSML-tags quetschen …

Nein. Das ergibt selbst dann kaum entzifferbare Bandwurmkonstruktionen, wenn  wir Hedda eine Verschnaufpause am Ende jeder Zeile zugestehen, wo es nicht so auffällt. Und überhaupt wäre es letztlich deutlich einfacher, den Text direkt selber einzusprechen. Wir müssen und werden einen Weg finden, der mit weniger Aufwand zum Ziel führt.

… im dritten Teil des Projektes.