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 1

Einleitung: Stefan liest

Nicht jede Lesung, nicht jede musikalische Interpretation eines mittelhochdeutschen Gedichts, die sich auf YouTube finden lässt, genügt wissenschaftlichen germanistischen Ansprüchen, aber wenn es nur darum geht, einmal einen Eindruck vom Klang der mittelhochdeutschen Sprache zu erhalten, dann wird man dort schnell fündig. Wer allerdings auf das Hören als Ersatz für das Sehen angewiesen ist, oder einfach nur den Wunsch verspürt, einen längeren mittelhochdeutschen Text anzuhören, anstatt ihn zu lesen, wird nicht nur auf den einschlägigen Plattformen vergeblich nach „Material“ suchen.

Das im folgenden Artikel vorgestellte Projekt versucht ein wenig Abhilfe zu schaffen. Es soll selbstverständlich nicht darum gehen, Hilfestellung bei der Einrichtung eines eigenen Tonstudios oder bei der Aufnahme eigener Lesungen zu geben. Nein, wir beabsichtigen, unseren Computer dazu zu bringen, uns einen mittelhochdeutschen Text vorzulesen und zwar so, dass wir ihn gut verstehen und seine Aussprache auch Fachleuten für das Mittelhochdeutsche keine Schmerzen verursacht. Die Ausgabe leiten wir dann in eine Audio-Datei, damit wir unser Hörbuch auf jedem uns genehmen Gerät abspielen können.

Das scheint auf den ersten Blick gar nicht so schwer zu bewerkstelligen: Schließlich lässt sich jedes Handy und jeder übliche Browser dazu bewegen, Texte vorzulesen. Dazu muss man allerdings teilweise eine zusätzliche App oder ein Add-On installieren. Das Betriebssystem eines Laptop- oder Desktop-Computer lässt sich natürlich ebenfalls um die Fähigkeit zur Sprachsynthese erweitern, wenn es diese nicht bereits von Haus aus mitbringt. Wer diesen Text beispielsweise auf einem Windows Rechner vor sich sieht, muss nur die  Strg-, die Windows– und die Enter-Taste gemeinsam betätigen, um die automatische Sprachausgabe zu aktivieren. Die auf meinem Rechner zu hörende Stimme von Windows10-Stefan klingt zwar manchmal etwas blechern, aber doch im Ganzen gut verständlich:

Ein ritter sô gelêret was,
daz er an den buochen las,
swaz er dar an geschriben vant:
der was Hartman genant,
dienstman was er zOuwe.

Nun ja, ein paar Worte hören sich etwas exotisch an  – ab und zu scheint Stefan bei fremdartigen Schreibungen zu vermuten, dass sie wohl englisch ausgesprochen werden müssen. Andere Wörter hingegen klingen viel zu vertraut. Kein Wunder! Stefan spricht und liest modernes Hochdeutsch und ahnt ja nicht, dass er in „dienstman“ einen Diphthong von sich geben soll. Probieren wir es gleich mit einem anderen Text als dem Armen Heinrich (Natürlich erkannt, oder?). Wie wäre es beispielsweise mit der Einleitung des Lanzelet des Ulrich von Zatzikhoven, eines ungefähren Zeitgenossen des gelehrten Ritters Hartmann von Aue?

Swer rehtiu wort gemerken kan,
der gedenke wie ein wîse man
hie vor bî alten zîten sprach,
dem sît diu welt der volge jach.

Das … klingt, zumindest von Stefan gelesen, schon weit weniger akzeptabel für mich. Jemand müsste Stefan ein paar Lektionen in der Aussprache mittelhochdeutscher Worte erteilen, bevor ich bereit wäre mir von ihm die fast 9500 Verse der an sich recht spannenden (und actionreichen!) Erzählung vom Artushof vorlesen zu lassen.

Dieser Gedanke soll der Ausgangspunkt für das heutige Projekt sein. Es gibt neben kommerziellen Angeboten zahlreiche Open-Source-Ansätze zur Spracherzeugung und -ausgabe, die Schnittstellen zur individuellen Anpassung und Programmierung bieten. Aber wir bleiben vorerst in der Microsoft-Umgebung und werden unsere Lösung in C# im Visual-Studio umsetzen. Größere Programmierkenntnisse brauchen wir dafür nicht und das ist der Hauptgrund für unsere Wahl. Zudem können wir uns in diesem Fall darauf verlassen, dass die Technologie stabil und fehlerfrei auf allen Rechnern läuft, auf denen Windows stabil und fehlerfr … Naja … auf denen halt …

Wer noch nie mit Visual-Studio oder C# gearbeitet hat, findet bei Verständnisfragen in reichem Maße Hilfe auf den Supportseiten von Microsoft. Dort kann er auch die (für private Zwecke) kostenlose Community-Version der Entwicklungsumgebung herunterladen:

Zum Download von Visual-Studio 2017

Als Übungstext verwenden wir nicht den gesamten Lanzelet-Roman, sondern beschränken uns vorerst bescheiden auf die ersten 49 Zeilen, die wir per copy & paste der Textfassung der Bibliotheca Augustana entnommen haben:

Lanzelet Verse 1 – 49

In Windows 10 per Rechtsklick das Kontextmenü öffnen und „Ziel speichern“ wählen, um den Text auf den eigenen Computer herunter zu laden und dort zu speichern. Jetzt ist alles vorbereitet. Wir können loslegen …

… und zwar im Teil 2.

Wer allerdings jetzt schon wissen will, was auf ihn zukommen wird, kann jetzt auch erst einen Blick auf den Tutorial-Überblick werfen:

Mein erstes mittelhochdeutsches Hörbuch – Teil 1 weiterlesen