Routinensammlung und Gewusst-wie für Mikrocontroller

Sprachausgabe

Für Telefonanwendungen sollte eine Sprachausgabe in den Mikrocontroller hineinpassen. Das erspart eventuell Displays und ähnliches. Um nicht extra Flash-Speicher anbauen zu müssen kommt nur synthetisierte Sprache in Frage, wie sie in den 80-er Jahren des letzten Jahrhunderts üblich war: Eine Roboterstimme mittels Formanten. Klingt altbacken, aber ist verständlich und erfüllt den Zweck. Leider ist dazu kaum Software aufzutreiben. Schon gar nichts was in einen 8-Bit-Mikrocontroller passt. Auch klingt alles (was es so gab und gibt) englisch. Eine Deutsch-Anpassung wäre hilfreich. Dazu ist eine Anpassung des Vokaldreiecks erforderlich. Das „Übersetzen“ von Text in Phoneme kann zur Compilezeit erfolgen, sodass der Mikrocontroller nur noch konstante Phoneme ausspucken muss.

Eine Alternative ist eine auf Sprachausgabe optimierte Audiokompression: LPC = Linear Predictive Coding nennt sich das, und dafür gibt es (zu spät, 2021 gefunden) eine Arduido-Implementierung: Talkie nennt die sich.

Ausgangspunkt für die phonembasierte Sprachausgabe ist eine auf der Softwaremüllhalde gefundene Abandonware, dessen Quelltext offenbar von einem Disassembler / Decompiler stammt, mühevoll zur Funktion gebracht aber bei weitem noch nicht ausgemistet.

Erste Überarbeitung

Hier ist der erste Entwicklungsstand abgelegt. Ausgangspunkt ist die Sprachausgabe für einen 32-Bit-Mikrocontroller namens ESP8266 mit WLAN auf dem Chip, wie's scheint. Ich brauche es allerdings für 8 Bit Verarbeitungsbreite.

Es ist soweit umgestrickt, dass:

Die (überflüssige) Phonemschrift stammt noch aus der Quelltextvorlage.

Der Binärphonem-String ist aus 3 64-Byte-Puffern entstanden (array-of-struct statt struct-of-array) und kann zweckmäßigerweise als Unicode-ähnliche wchar_t-Zeichenkette abgelegt werden.

Zweite Überarbeitung

Immer noch darauf aus, den Wust auseinanderzunehmen, habe ich das Programm in 4 Teilprogramme aufgeteilt, die dazu bestimmt sind, in eine Pipe verkettet zu werden. Das Kunswerk ist diesmal nur für (das gute alte) Visual Studio 6 gemacht. Um es selbst zu kompilieren benötigt man mein msvcrt-light-Paket. Denn nur so ist sichergestellt, dass das Kompilat auch wirklich nur aus selbstgeschriebenem Kode besteht. Andernfalls wirft der Kompiler mehr oder weniger viel Microsoft-Kode hinein, 30 Kilobyte Minimum.

Nur das dritte Teilstück muss in einen AVR implementiert werden. Da sogar das Windows-Programm (die „Echse“) < 8 KByte groß ist, passt es sicherlich mühelos in einen ATmega32-8 oder -U4, also Arduino Uno oder Leonardo. Natürlich nicht diese „Echse“, sondern das Ergebnis der Kompilierung des Quelltextes mit avr-gcc. Entsprechende PROGMEM-Attribute für die Tabellenablage im Flash-Speicher und pgm_read_byte()s zum Lesezugriff darauf sind bereits eingebaut.

Immer noch sind eine Unmenge gotos mit Sprungmarken aus Hexadezimalzahlen sowie globale Variablen drin. Das auszumisten wird noch zwei weitere Überarbeitungen erfordern, ganz zu schweigen von der Aufgabe, das ganze etwas mehr deutsch klingen zu lassen.

Die Teile im einzelnen:

Nun, wozu das ganze?
Der Testfall ist echo Can you hear me now? | s1 | s2 | s3 | s4! Die Zeichenkette Can you hear me now? wird zum Eingabetext für s1.exe, dessen Ausgabe die Eingabe für s2.exe usw. bis es aus dem Lautsprecher schallt.

Um das ganze auf einen Arduino zu bringen erscheint es wenig sinnvoll, die Programme s1.exe und s2.exe zu implementieren, es sei denn, man steht auf Englisch und hat noch viel Flash-Speicher frei. Denn niemand wird sich von einem 8-Bit-Controller variablen Text vorlesen lassen. Schon gar nicht Namen aus einem Telefonbuch. Feste Texte, wie bspw. Zahlen, genügen vollauf.

Die Datei phonems.bin kann als Eingabedatei von s3.exe dienen, als eine Art Abkürzung: s3 < phonems.bin | s4 Und phonems enthält zwei menschenlesbare UTF-16-Beispiel-Phonemstrings.

Im Wettrennen: LPC

Durch den Verweis auf LPC = Linear Predictive Coding angestachelt, anscheinend eine TI-Erfindung? (Nein, allenfalls die geänderte Bitkodierung zur weiteren Speicher-Einsparung, die Grundlage ist LPC10 bzw. FIPS137), soll Sprachausgabe auch mit einem ATtiny85 möglich sein. Nein, die Unmachbarkeit von schnellen Multiplikationen ohne MUL-Befehl, daran glaube ich nicht. Wenn die Framerate für den Multiplikationsblock 8 kHz beträgt, hat man bei 16 MHz 2000 Takte Zeit. Macht bei 20 Multiplikationen 100 Takte. Das reicht geradeso.

Synchronansatz von Talkie, funktioniert mit ATmega32U4, zur Reduktion der Interruptlast und des Speicherhungers. Arduino-Bezug entfernt und die vielen #ifdefs entfernt. Aus einem faktischen Singleton ein „statisches Objekt“ gemacht. (Es ist bekanntlich relativ egal, ob man in C++ ein Objekt mit lauter statischen Memberfunktionen bastelt oder Funktionen und Variablen in einen Namespace tut. Abgesehen vom dann fehlenden Defaultkonstruktor. Aber darauf kann/sollte man bei Mikrocontrollerprojekten verzichten können. Daher ist's hier mittels Namespace gelöst.) Das Beispiel würde auf einem ATmega32U4 2,5 KB Kode und für die Zahlen und einige Worte 1,8 KB Flash-Tabellenplatz benötigen.

Die Software implementiert ein Latticefilter mit unendlicher Impulsantwort (IIR) 10. Ordnung in der folgenden Form:
      ↓Subtraktion  ┌Spannung u[8]
    ╔═══╗    ╔═══╗  ↓   ╔═══╗      ╔═══╗      ╔═══╗      ╔═══╗      ╔═══╗      ╔═══╗      ╔═══╗      ╔═══╗
→u10╢ - ╟→u9─╢ - ╟─→u8─┬╢ - ╟─→u7─┬╢ - ╟─→u6─┬╢ - ╟─→u5─┬╢ - ╟─→u4─┬╢ - ╟─→u3─┬╢ - ╟─→u2─┬╢ - ╟─→u1─┬╢ - ╟─→u0─┬───┬→
    ╚═╤═╝    ╚═╤═╝┌×K8←┘╚═╤═╝┌×K7←┘╚═╤═╝┌×K6←┘╚═╤═╝┌×K5←┘╚═╤═╝┌×K4←┘╚═╤═╝┌×K3←┘╚═╤═╝┌×K2←┘╚═╤═╝┌×K1←┘╚═╤═╝┌×K0←┘   │
      └×K9←┐   └←─┼×K8┐   └←─┼×K7┐   └←─┼×K6┐   └←─┼×K5┐   └←─┼×K4┐   └←─┼×K3┐   └←─┼×K2┐   └←─┼×K1┐   └←─┼×K0┐    │
        ↑  │╔══╗╔═╧═╗ │╔══╗╔═╧═╗ │╔══╗╔═╧═╗ │╔══╗╔═╧═╗ │╔══╗╔═╧═╗ │╔══╗╔═╧═╗ │╔══╗╔═╧═╗ │╔══╗╔═╧═╗ │╔══╗╔═╧═╗ │╔══╗│
        │  └╢x9╟╢ + ╟─┴╢x8╟╢ + ╟─┴╢x7╟╢ + ╟─┴╢x6╟╢ + ╟─┴╢x5╟╢ + ╟─┴╢x4╟╢ + ╟─┴╢x3╟╢ + ╟─┴╢x2╟╢ + ╟─┴╢x1╟╢ + ╟─┴╢x0╟┘
        │   ╚══╝╚═══╝  ╚══╝╚═══╝  ╚══╝╚═══╝  ╚══╝╚═══╝  ╚══╝╚═══╝  ╚══╝╚═══╝  ╚══╝╚═══╝  ╚══╝╚═══╝  ╚══╝╚═══╝  ╚══╝
        └Multiplikation            ↑Speicherstelle=Verzögerung           ↑Addition

Das AVR-Programm als JavaScript:

Bitanalyse des gesprochenen Wortes
Bits:4165544444333200 × 8200 × 10
frame #energyrepeatperiodK0K1K2K3K4K5K6K7K8K9Signal 25 msWasserfall IIR

Es fiel (im Debugger) auf, dass die Amplitude der originalen Talkie-Firmware viel zu gering ist, Spitzenwerte liegen bei 25 %. Das ist beim Arduino verschenkte Ausgangsleistung. Die ursprünglichen Daten führen zur Instabilität im Filter bei der Ziffer Acht, ich habe die Daten leicht frisiert: Den Faktor K0 reduziert. Die Ausgangsspannung tendiert zu stark zu positiven Werten.

Nunmehr wurde das Kettenfilter auf 8 Bit reduziert und jede Additionsstufe mit Sättigung ausgestattet (das kostet dem AVR nicht so viel). Damit konnte die Ausgabelautstärke ohne hörbare Übersteuerungseffekte auf das Vierfache(!) gesteigert werden. Mit der Bitbreitenreduktion geht eine Verdopplung der Multiplikationsgeschwindigkeit einher, sodass sich dieses Kettenfilter auch auf einem ATtiny (der bekanntlich keinen MUL-Befehl hat) mit 16 oder gar 8 MHz Taktfrequenz betreiben lässt. Was zu beweisen wäre. Diese Javascript-Ausgabe simuliert genau ein derart konstruiertes Latticefilter.

Viel schwieriger als die Dekompression ist die Kompression. Die von mir gefundene Implementierung von 2011 Peter Knight in freemat habe ich auf Javascript umgestellt. Damit kann jedermann gesprochene Texte im Browser komprimieren. Zum Talkie-Kompressor