Dinodialogomat: Audio von MP3-Player steuert Mundbewegung mit Servos (🚧)

Zwei Dinofiguren treten in Dialog und ihre Münder werden über Servos synchron zur Sprache bewegt. Ein MP3-DFPlayer Mini liefert das Audio-Signal, das über Analogeingänge des Arduino in Servosignale umgewandelt wird (Schwätzomat).
Der Start des Automaten geschieht über einen Ultraschallsensor.
Um bei 8 Bit-µC wie Arduino UNO, NANO und MEGA Speicher- und Rechenzeit zu sparen wurden die Speicherung und Berechnungen überwiegend mit 8 Bit realisiert. Z.B. werden die Servopositionen durch 10 geteilt gespeichert: 1500µs -> 150 Speicherwert mit 1 Byte statt 1500 mit 2 Byte.

  1. Hardware
  2. DFPlayer Mini
  3. Schätzomat steuert mit Audio Mundbewegungen
  4. Automatensoftware des Dinodialogomat (🚧)
Dinodialogomat Prototyp
Dinodialogomat Prototyp

Blockschaltbild der Prototypschaltung

Dinodialogomat Blockschaltbild Anschluss von DFPlayer mini, Servos, Poti und Taster
Dinodialogomat Blockschaltbild

Die Pin-Belegungen sind hier nicht hübsch geraten, bis auf die Analog-Eingänge aber frei wählbar. Die Münder der Figuren werden durch Servos bewegt. Der DFPlayer Mini ist über eine Software Serielle Schnittstelle verbunden, ein Widerstand R1 ist notwendig, weil der Player intern auf 3,3V läuft, der Arduino UNO liefert jedoch 5V, durch den Widerstand wird der Strom begrenzt. Die Stereoausgänge des Players sind mit den Analogeingängen des Arduino verbunden. Mit einem Ultraschallsensor wird gemessen, ob sich eine Person vor dem Automat befindet. Das Poti R4 dient zur Lautstärke-Einstellung und um ggfs. die Endpositionen der Servos in einem Setup justieren zu können. Der Taster S1 steuert den Servojustage-Ablauf.

Servos

Die verwendeten Servos benötigen einen kräftigen Strom >1A, daher bekommen sie eine unabhängige 5V Spannungsversorgung.
Beim Einschalten bekommen die Servos noch kein Servosignal, normalerweise tun sie dann auch nichts, leider fahren meine Servos nach dem Einschalten in eine Position, bei der das Getriebe krachende Geräusche von sich gibt.

DFPlayer Mini und Library DFPlayer Mini Fast

Links zum Player: [🔗 wiki.dfrobot.com/DFPlayer_Mini_SKU_DFR0299] [🔗 Datenblatt]
[https://wolles-elektronikkiste.de/dfplayer-mini-ansteuerung-mit-dem-arduino] (Gute Beschreibung der Verzeichnisstruktur der SD-Karte)

Bei den meisten Anwendungen des Players speichere ich die Audiodateien im mp3-Ordner. Die Anzahl der Dateien ist bekannt, bzw. es ist klar welche Datei abgespielt werden soll. Bei diesem Projekt soll der Automat jedoch selber aus einer beliebigen Anzahl von Stücken auswählen können. Auch wäre es praktisch bei der Inbetriebnahme für die Servo-Justage Ansagen zu hören, statt einen Rechner für die Ausgabe der Justage-Anweisungen zu benötigen. Die Justage-Ansagen sollen in einem Unterordner untergebracht werden.

Dateien auf SD-Karte im „mp3“ Verzeichnis speichern

Dateistruktur: Dateien sollten mit Nummern wie 0001.mp3 gespeichert sein.
Wenn die Dateien im Root- (Haupt-) Verzeichnis abgelegt werden, gibt die Reihenfolge beim Kopieren auf die SD-Karte die Nummerierung vor, unabhängig vom Dateinamen. In Unterverzeichnissen wie „mp3“ wird dagegen die Nummer im Dateinamen beachtet. Hinter den Nummern dürfen durchaus weitere beschreibende Texte stehen, siehe Beispiel rechts.

Dateien im Unterverzeichnis mp3 werden mit playFromMP3Folder(nummer) abgespielt.
Die Benennung ist nach dem Schema 0001xxx.mp3.

Dateistruktur SD-Karte
Dateistruktur SD-Karte

Verwenden von Unterverzeichnissen „01“..“99″

Da ich Audio-Dateien zur Justage der Servos vorgeben möchte und die Anzahl der Spielstücke nicht bekannt ist, dachte ich mir, dass zwei Verzeichnisse dafür sinnvoll wären. Ein Verzeichnis 01 mit den Spielstücken und ein Verzeichnis 02 mit meinen Justageanweisungen.

Unterverzeichnisse werden mit 01..99 benannt. In den Verzeichnissen werden die Stücke mit diesem Schema benannt: 001xxx.mp3
Abgespielt werden die Stücke mit playFolder(verzeichnis,stück).

Die Anzahl der Spielstücke wollte ich mit numTracksInFolder(i) ermitteln, das funktionierte nicht wie erwartet, es wird nur die Anzahl des Ordners zurück gegeben, der zuletzt gespielt wurde, auch bei der DFRobotDFPlayerMini-Lib war das nicht anders.

MacOS Dot-Clean

Eine blöde Sache ist das Hinterlegen von „unsichtbaren“ Dateien auf der SD-Speicherkarte durch das Betriebssystem, der Player lässt sich dadurch irritieren.
Eine Lösung ist hier zu finden: wiki.dfrobot.com/DFPlayer_Mini_SKU_DFR0299#target_6🔗

Welche Arduino Library zur Ansteuerung des Players soll ich nehmen?

Die Kommunikation zum Player läuft über eine serielle Schnittstelle. Es werden Kommandos wie Play Next: [7E FF 06 01 00 00 00 FE FA EF] darüber gesendet und es werden im gleichen Format Antworten vom Player zurück gesendet siehe [🔗 Datenblatt].

Die Librarys erledigen dies Nutzerfreundlich, man muss sich nicht selber um die Kommandos kümmern. Allerdings kann es dabei Probleme mit der seriellen Kommunikation geben, das Timing ist nicht ohne. Ausserdem verhalten sich einige Kommandos nicht wie erwartet. In den Issues auf Github gibt es einige „Tränen“..
Z.B. Anzahl der Dateien in einem Verzeichnis bestimmen funktioniert nicht richtig: [🔗 forum.arduino.cc/t/dfplayer-anzahl-der-dateien-im-verzeichnis/926305/11]

Abfragen ob der Player gerade spielt

Es gibt zwei Möglichkeiten dafür:

  1. myMP3.isPlaying() wird über die Serielle Schnittstelle gemacht, braucht aber 64ms!
  2. Über den BUSY-Pin am Player, solange er Low ist spielt der Player (gewählt).
Library DFPlayerMini_Fast
Library DFPlayerMini_Fast

Schwätzomat steuert mit Audio Mundbewegungen

Wikipedia: schwätzen🔗 Seit 1989 baue ich Schwätzomaten-Elektronik, damals wurden noch mit Analogtechnik Hubmagneten angesteuert. Schwätzenden Figuren soll der Mund möglichst passend zur Sprache animiert werden. Eine Herausforderung war und ist die Qualität des Audiomaterials, schwankende Lautstärke und Rauschen, damals mit Kassettenspielern🔗 heute mit MP3 müssen ausgeglichen werden.

AD-Wandler Referenzspannung einstellen

Die Analogsignale des Players und des Potentiometers werden mit dem AD-Wandler des Arduino gemessen. Als Spannungsreferenz dient normalerweise die Versorgungsspannung des µC, beim Uno 5V. Siehe [arduino.cc/reference/en/language/functions/analog-io/analogreference/]
Die Auflösung ist dabei typischerweise 10 Bit -> 0..1023 als Messwert. Siehe [https://docs.arduino.cc/language-reference/de/funktionen/analog-io/analogRead/]
Nachteilig ist dabei, dass für kleine Messwerte die AD-Wandlerwerte klein sind und ausserdem Spannungsschwankungen und Rauschen der Versorgungsspannung die Messungen verschlechtern. Daher verwende ich die interne 1,1V Referenz des ATmega328P (UNO, NANO).
Im setup() eingestellt mit analogReference(INTERNAL); Die Referenzspannung wird dabei auf dem AREF-Pin des µC ausgegeben an den ich das Poti angeschlossen habe.
Bei einem anderen µC ist die Referenzspannungseinstellung entsprechend an zu passen, z.B. beim Arduino MEGA auf INTERNAL1V1.
Die Lautstärke des DFPlayers-Players sollte nicht <20 eingestellt werden, da die Signale dann zu klein werden.

Schwätzomat Audio-Messprinzip

  • Die Servos werden mit 50Hz PWM-Signal angesteuert, öfter als 50 mal pro Sekunde einen neuen Wert ein zu stellen macht keinen Sinn.
  • Eine AD-Wandlung mit analogRead() und Verarbeitung braucht ca. 130 µs. Bei zwei Kanälen sind das 260 µs, die Abtastfrequenz ist ca. 3,8 kHz. Bis ein neues Servosignal ausgegeben werden soll können ca. 76 Messungen vorgenommen werden- nenne ich Epoche.
  • Soll der Mittelwert der Messungen oder der Maximalwert einer Epoche verwendet werden? Ich habe mit dem Maximalwert bessere Ergebnisse erzielt.
  • Die Lautstärke der Stücke ist nicht gleich, die Lautstärke des MP3-Players kann verändert werden. Während das Stück läuft ermittle ich ständig das Maximum des Kanals und passe die Servoausschläge entsprechend an.
  • Es gibt ein Grundrauschen (DA-Wandler des DFPlayers rauscht!) bei den Stücken, die Münder würden dadurch nicht geschlossen. Ausserdem ermittle ich den Minimalwert und ziehe ihn vom Epochen-Audiowert ab.
  • Leisere Passagen führten zu sehr geringen Servoausschlägen, der Mund bewegte sich kaum. Bei niedrigen Audio-Werten (<maxAudio/2) wird nun der Audiowert mit 2 multipliziert verarbeitet.
  • Der Audiowert einer Epoche wird mit dem Maximalwert des Stückes auf die Servopositionen mundZu, mundOffen umgerechnet (mapping).

Ausgabe der Epochen-Werte mit Seriellem Plotter.
Siehe Software-Doku..

Epochen - Audio-Signalverlauf Rechts spricht
Epochen – Signalverlauf Rechts spricht

Um die Messungen auch mit 8 Bit µC effizient durchführen zu können habe ich ein wenig gebastelt. ToDo: 16-Bit Variante erstellen und testen ob sie wirklich schlechter ist.
Die Servos bekommen nur während ein Stück abgespielt wird ein PWM-Signal. Analog-Servos sind dadurch zwischen den Stücken „entspannt“ und verbrauchen weniger Strom. Bei Digital-Servos muss das Verhalten überprüft und ggfs. angepasst werden.

Mund-Servos justieren

Die Servos bewegen die Münder recht dynamisch, daher ist es wichtig die Endpositionen mundZu und mundOffen so ein zu stellen, dass kein Verschleiß provoziert wird. Typischerweise befinden sich die Servos bei 1,5 ms bzw. 1500 µs in der Mitte. Folgend zwei Varianten die Servos zu justieren:

Werte mit Servotester ermitteln und einprogrammieren

Mit einem Servotester wie [3.2 Servo steuern mit PWM] oder [mezdata.de/avr/370_servotester/index.php] oder aus dem Handel können die Endpositionen ermittelt werden.
Der Servo wird mit 1500 µs angesteuert und dann der Servohebel so aufgesetzt, dass der Mund dabei in der Mitte steht. Danach werden die Werte für mundZu und mundOffen ermittelt und können in der Software eingetragen werden.

// Endpositionen der Servos für mundZu und mundAuf kennen z.B. 1800µs -> 180 eintragen
Schwaetzo schwaetzoL(A1,9,180,140); // AudioIn, ServoOut, mundZu, mundOffen
Schwaetzo schwaetzoR(A0,8,170,130);

Werte durch Justage am Automaten ermitteln und in EEPROM speichern

Bei dieser Methode sind die Servodaten noch nicht bekannt, in der Software wird dieser Code verwendet:

Schwaetzo schwaetzoL(A1,9); // AudioIn, ServoOut, noch unjustiert
Schwaetzo schwaetzoR(A0,8);

Hinweis: In dieser Version gehe ich davon aus, dass eine Verbindung zum Seriellen Monitor des Arduino (115200 Baud) besteht. Angedacht aber noch nicht realisiert ist eine Version, die die Anweisungen als MP3 Ausgabe spricht.
Bei dieser Methode wird bei Erstinbetriebnahme entweder vor Einschalten der Spannung der Taster gedrückt oder im Seriellen Monitor ein j zum Automaten gesendet, danach befindet sich der Automat im Servo-Justage-Modus, hier die Ausgaben auf dem Seriellen Monitor (bzw. Audioausgaben):

  1. Bitte bei Erstjustage alle Servohebel von den Achsen abziehen! Dann Taste drücken.“ Nach Tastendruck werden die Servos in die Mitte gefahren.
  2. „Beide Servohebel so aufsetzen, dass der jeweilige Mund halb geöffnet ist. Dann Taste drücken.“
  3. „Bitte Poti in die Mitte stellen. Dann Taste drücken.“ Das Poti steuert die Servos bis zu den Maximalpositionen, um Beschädigungen zu vermeiden sollte es dabei nahe der Mitte 160..140 stehen.
    • „Poti steht nicht in der Mitte: 170“ Poti zu Mitte bewegen
  4. „Linken Servo in MundZu Position einstellen. Dann Taste drücken.“ Mund sollte zu sein, aber die Lippen müssen nicht aufeinander gepresst sein 😉.
  5. „Linken Servo in MundOffen Position einstellen. Dann Taste drücken.“ Mund sollte offen sein, aber den Kiefer nicht ausrenken.
  6. „Bitte Poti in die Mitte stellen. Dann Taste drücken.“ Siehe 3.
    • „Poti steht nicht in der Mitte: 130“
  7. „Rechten Servo in MundZu Position einstellen. Dann Taste drücken.“
  8. „Rechten Servo in MundOffen Position einstellen. Dann Taste drücken.“
  9. „Servos justiert.“ Die ermittelten Werte werden im EEPROM gespeichert und bei jedem Neustart des Automaten wieder daraus geladen.

Synopsis: [arduino.cc/en/Tutorial/LibraryExamples/EEPROMWrite] [docs.arduino.cc/learn/built-in-libraries/eeprom/]

Schwaetzo-Audio-Library

// SchwaetzoLib.h V1.2 © 29.12.2024 Oliver Mezger MezMedia.de CC BY-SA 4.0
#ifndef SchwaetzoLib_h
#define SchwaetzoLib_h
#endif

#include "Arduino.h"
#include <Servo.h>
#define DEFAULT_MIN_AUDIO 20 // Voreinstellung Minimaler Audiolevel bei rauschenden Aufnahmen
#define DEFAULT_MAX_AUDIO 60 // Voreinstellung Maximaler Audiolevel
#define STILL_SCHWELLE 3     // Schwelle 1/4 Maximum (nicht verwendet)

class Schwaetzo{
  public:
    Schwaetzo(byte ein,byte aus); // AudioEingang ServoAusgang
    Schwaetzo(byte ein,byte aus,byte mzu, byte moffen); // AudioEingang ServoAusgang Servopositionen
    void setMundZu(byte zu);      // Servoposition für mundZu einstellen
    void setMundOffen(byte auf);  // Servoposition für mundAuf einstellen
    void go(); // periodisch aufrufen um Analog einzulesen und Servo zu steueren
    void an(); // Servo wird angeschlossen
    void aus();// Servo wird deaktiviert
    byte getMaxAudio();  // Audio Maximalwert
    byte getLastAudio(); // letzer ermittelter Audiowert für Servo
    void moveServo(byte n); // Servo an Position n bewegen zur Justage
    void schreibeEEPROM();  // Servodaten ins EEPROM schreiben
    void leseEEPROM();      // Servodaten aus EEPROM lesen
  private:
    static byte instanzen;           // Klassenvariable
    byte instanzNummer;              // die Schwaetzoinstanzen werden durchnummeriert für EEPROM Adressen
    Servo servo;                     // Servo-Instanz
    byte audioEingang;               // AnalogPin fuer Sound
    byte servoAusgang;               // Servoanschluss
    byte mundZu = 150;               // Servoposition wenn Mund zu {100..200}
    byte mundOffen = 150;            // Servoposition wenn Mund offen {100..200}
    byte servoWeg = 0;               // mundOffen-nunZu
    byte soundSampels = 0 ;          // Zaehler fuer Messungen in einer Epoche
    byte minAudio = DEFAULT_MIN_AUDIO; // leisester Wert eines Stueckes (Rauschen)
    byte maxAudio = DEFAULT_MAX_AUDIO; // lautester Wert eines Stueckes
    uint16_t audio = 0;              // lautester Wert von n Messungen
    byte audioShift = 1;             // analogReadWert >> 1 wird bei lautem Audio erhöht um innerhalb 8 Bit zu bleiben
    byte lastAudio = 0;              // für Ausgabe auf Seriellem Plotter
};
// SchwaetzoLib.cpp V1.2 © 29.12.2024 Oliver Mezger MezMedia.de CC BY-SA 4.0

#include "Arduino.h"
#include "SchwaetzoLib.h"
#include <EEPROM.h>

byte Schwaetzo::instanzen = 0; // Klassenvariable für Instanznummer

Schwaetzo::Schwaetzo(byte ein,byte aus){ // AudioEingang ServoAusgang für Verfahren Servo-Justage mit Automat
  instanzNummer = instanzen++; // die Instanzen nummerieren wegen EEPROM
  audioEingang = ein;
  servoAusgang = aus;
  pinMode(aus, OUTPUT);  // Servosignal = 0
  digitalWrite(aus,LOW); // einstellen
}

Schwaetzo::Schwaetzo(byte ein,byte aus,byte mzu, byte moffen){ // AudioEingang ServoAusgang MundZu MundOffen
  Schwaetzo(ein,aus);
  setMundZu(mzu);
  setMundOffen(moffen);
}

void Schwaetzo::setMundZu(byte zu){     // Servoposition für mundZu einstellen
  if (zu>=100 && zu<= 200) mundZu=zu;
  else{
    Serial.print(F("setMundZu: Wert passt nicht: "));
    Serial.println(zu);
  } 
  servoWeg = abs(mundOffen-mundZu);
}

void Schwaetzo::setMundOffen(byte auf){     // Servoposition für mundOffen einstellen
  if (auf>=100 && auf<= 200) mundOffen = auf;
  else{
    Serial.print(F("setMundOffen: Wert passt nicht: "));
    Serial.println(auf);
  } 
  servoWeg = abs(mundOffen-mundZu);
}

void Schwaetzo::go(){ // Sample aufnehmen braucht 130µs somit bei 2 Kanälen 3,846 kHz Abtastfrequenz
  static uint16_t n;
  if (soundSampels >=100){ // wurden genug Sampels in Epoche aufgenommen 3846Hz/50Hz=77 
    soundSampels = 0;
    if (audio > 255){ // wenn es zu laut ist Messungen abschwächen
      audio = 255;
      audioShift++;   // mehr Vorteilen, Messwert halbieren
      minAudio = DEFAULT_MIN_AUDIO;
      maxAudio = DEFAULT_MAX_AUDIO;
    } else {
      if (audio > maxAudio) maxAudio = audio;  // Maximallautstärke des Stückes merken
      if (audio < minAudio) minAudio = audio;  // Minimallautstärke des Stückes merken
      if(audio>=minAudio) audio -= minAudio;   // Rauschen abziehen
      if (audio < maxAudio/2){  // leise Stellen verstärken
        n = audio * servoWeg * 2 / maxAudio; // Servoausschlag berechnen
      } else {
        n = audio * servoWeg / maxAudio;     // Servoausschlag berechnen
      }
      if(n>servoWeg) n = servoWeg; // Servoausschlag begrenzen
      if(mundOffen>mundZu) n = mundZu + n;
      else n = mundZu - n;
      servo.writeMicroseconds(n*10); // Servo Pulsweite ausgeben
    }
    /*if (audio > (maxAudio>>stillSchwelle)){ // Schwelle 1/8 Maximum
    //sprachSituation |= 1;
    //ausTimer =0;
    }*/
    lastAudio = audio; // letzten Wert merken
    audio=0;
  }
  else{ // Audio-Messung
    n = analogRead(audioEingang)>>audioShift; // Audiosignal lesen 
    if(n>audio) audio=n; // Maximum finden
    soundSampels++;
    lastAudio=0;
  }
}

void Schwaetzo::an(){  // Servo wird angeschlossen, bekommt Signal
  minAudio = DEFAULT_MIN_AUDIO; // neues Stück neues Glück
  maxAudio = DEFAULT_MAX_AUDIO;
  audio = 0;
  soundSampels = 0;
  lastAudio = 0;
  audioShift = 1; // Abschwächen von analogRead
  servo.attach(servoAusgang,1000,2000);  // Servo anschließen 
  Serial.print(F("Servo angeschlossen "));
  Serial.println(servoAusgang);
}

void Schwaetzo::aus(){  // Servosignal wird abgeschaltet
  servo.writeMicroseconds(mundZu*10); // Mund schließen 
  delay(100);                         // warten bis ausgeführt
  servo.detach();                     // Servosignal abschalten
  Serial.print(F("Servo aus, audioShift: ")); // Infos über das Stück ausgeben
  Serial.print(audioShift);
  Serial.print(F(" MinLevel: "));
  Serial.print(minAudio);
  Serial.print(F(" MaxLevel: "));
  Serial.println(maxAudio);
}

byte Schwaetzo::getMaxAudio(){ // maximaler Audiowert
  return maxAudio;
}

byte Schwaetzo::getLastAudio(){ // letzter Audiowert, 0 während Messung
  return lastAudio;
}

void Schwaetzo::moveServo(byte n){ // Servo einstellen während Justage
  servo.writeMicroseconds(n*10);
}

void Schwaetzo::schreibeEEPROM(){  // Servodaten ins EEPROM schreiben
  EEPROM.update((instanzNummer+1)*2, mundZu);
  EEPROM.update((instanzNummer+1)*2+1, mundOffen);
}

void Schwaetzo::leseEEPROM(){      // Servodaten aus EEPROM lesen
  byte i;
  i = EEPROM.read((instanzNummer+1)*2);
  setMundZu(i);
  i = EEPROM.read((instanzNummer+1)*2+1);
  setMundOffen(i);
}

Automatensoftware des Dinodialogomat

Die Audiostücke werden aus dem mp3-Verzeichnis wieder gegeben, Audio-Anweisungen zur Servo-Justage sind noch nicht implementiert.

  • Entfernungsmessung mit Ultraschall
  • Einbinden Schwaetzo
  • Wecker, nichtunterbrechende Zeitverzögerung
  • Zustandsautomat, enum

Bedienung über Seriellen Monitor

Über die serielle Schnittstelle (115200 Baud) kann der Automat gesteuert werden:

? Hilfe Alle Kommandos werden angezeigt
p Pause Der Player bekommt Pause-Befehl
s Stopp Der Player bekommt Stopp-Befehl
a1,1 Audiotest mit Ausgabe auf Serieller Plotter aus Verzeichnis 1, Stück 1
v1,1 Spiele aus Verzeichnis 1, Stück 1 a0,1 würde aus mp3-Verzeichnis spielen
j Servojustage

Rauschender DFPlayerMini

Wenn ein Stück mit Pause-Befehl gestoppt wird zeigt die Ausgabe einen MinLevel von ca. 8..13 an. Läuft das Stück regulär zu Ende oder wird mit dem Stopp-Befehl gestoppt ist MinLevel 0. Bei Pause wird der DA-Wandler nicht abgeschaltet und sein Rauschwert, bzw. der Rauschwert der Aufnahme wird ausgegeben. Sonst wird der DA-Wandler abgeschaltet und dabei wird MinLevel korrekterweise 0. Zum Testen wie stark eine Aufnahme zusätzlich zum DA-Wandler rauscht während der Wiedergabe den Pause-Befehl senden. Beim Abspielen oder Audiotesten wird der Test ob der Player noch spielt und die Lautstärkeeinstellung durch die Potiposition nur jede Sekunde durchgeführt und die Sampling-Rate durchgängig hoch zu halten.

// Dinodialogomat V0.5 © 29.12.2024 Oliver Mezger MezMedia.de CC BY-SA 4.0
#include <SoftwareSerial.h>
#include <DFPlayerMini_Fast.h>
#include <EEPROM.h>
#include "SchwaetzoLib.h"

//#define DEBUG_ME 1  // Zum Messen der Ausführungsdauer von Schwaetzo:go()
#define TRIGGER 12 // Ultraschall: Digitalpin 12 zum Auslösen einer Entfernungsmessung
#define ECHO 13    // Ultraschall: Pin 13 empfängt den Messimpuls
#define BUSY 2     // MP3-Player: Busy-Pin, low aktiv
#define POTI A2    // Pin des Potentiometers
#define TASTER 3   // Taster gegen GND zu Justieren
#define NAHE_ENTFERNUNG 60 // beim Unterschreiten ist Person nahe
#define EEPROM_ERKENNUNG 42 // falls schon programmiert dann hat EEPROM Adresse 0 diesen Wert
SoftwareSerial mySerial(10, 11); // RX, TX, fuer DF-Player
DFPlayerMini_Fast myMP3;
// Endpositionen der Servos für mundZu und mundAuf kennen z.B. 1800µs -> 180 eintragen
//Schwaetzo schwaetzoL(A1,9,180,140); // AudioIn, ServoOut, mundZu, mundOffen
//Schwaetzo schwaetzoR(A0,8,170,130);
Schwaetzo schwaetzoL(A1,9); // AudioIn, ServoOut, noch unjustiert
Schwaetzo schwaetzoR(A0,8);

enum zustandstyp {RUHE_ENTRY,RUHE,NAHE,SPIELEN,AUDIO_TEST,J_ANFANG,J_SERVO_ABGEZOGEN,J_SERVO_AUFSETZEN,J_POTI_MITTE,J_LINKS_ZU,J_LINKS_AUF,J_POTI_MITTE2,J_RECHTS_ZU,J_RECHTS_AUF}; // definiere Aufzählungstyp
enum zustandstyp zustand = RUHE_ENTRY; // Definiere und initialisiere Variable
byte lautstaerke = 20; // 0..30
byte stueck=1; // das zu spielende Stueck

void einstellenLautstaerke(){ // Poti einlesen und MP3 Lautstärke einstellen
  unsigned int l = 20+analogRead(POTI)/100; // POTI einlesen und 0..1023 auf 20..30 umsetzen
  if (l>30) l=30;
  if (l!=lautstaerke){
    lautstaerke=l;
    Serial.print(F("Neue Lautstaerke: "));
    Serial.println(l);
    myMP3.volume(l);
  }
}

void setup() {
  byte eeAdr = 0;            // EEPROM-Adresse
  Serial.begin(115200);      // Ausgabe ueber serieller Monitor
  mySerial.begin(9600);      // Anbindung des DF-Player
  myMP3.begin(mySerial);     // gibt immer true aus
  analogReference(INTERNAL); // A/D Referenzspannung https://www.arduino.cc/reference/en/language/functions/analog-io/analogreference/
  einstellenLautstaerke();
  delay(1000);
  pinMode(TRIGGER, OUTPUT); // Trigger-Pin ist ein Ausgang
  pinMode(ECHO, INPUT);     // Echo-Pin ist ein Eingang
  pinMode(BUSY,INPUT);      // Abfragen ob MP3-Player spielt
  pinMode(TASTER,INPUT_PULLUP);
  Serial.print(F("Anzahl Ordner auf SD-Karte: "));
  Serial.println(myMP3.numFolders());
  if (!digitalRead(TASTER)){ // wenn Taster gedrückt nach Reset
    zustand = J_ANFANG;
    //gedruecktTaster();
  }
  if (EEPROM.read(0)==EEPROM_ERKENNUNG){  // falls EEPROM Servodaten hat
    Serial.print(F("EEPROM hat Servodaten"));
    Serial.print(F("EEPROM: "));
    for (eeAdr=0;eeAdr<6;eeAdr++){  // alle Daten ausgeben
      Serial.print(EEPROM.read(eeAdr));
      Serial.print(F(" "));
    }
    Serial.println();
    schwaetzoL.leseEEPROM(); // Servodaten lesen
    schwaetzoR.leseEEPROM();
  } else {
    Serial.print(F("EEPROM ist Jungfrau"));
  }
}

unsigned int messeEntfernung(){    // Messung ob jemand vor dem Automat steht Rückgabe in cm
  static unsigned int entfernung=0; // persistente lokale Variable für Entfernung in cm
  unsigned int mess;
  digitalWrite(TRIGGER, HIGH); // Trigger-Pin high zum Start der Messung
  delayMicroseconds(10);       // 10 µs reichen 
  digitalWrite(TRIGGER, LOW);  // Trigger-Pin low
  mess = pulseIn(ECHO, HIGH,12000); // auf 12ms begrenzen für max 206cm
  mess = mess/58; // näherungsweise Berechnung der cm 
  if (mess==0||mess>200){
    entfernung = 200;
  }
  else{
    entfernung = (entfernung+mess)/2; // Messungen glätten
  }
  //Serial.println(entfernung);
  return entfernung;
}

unsigned long weckzeit; // wann der Wecker "klingeln" soll
void stelleWecker(unsigned int n){ // n ist Zeit in ms
  weckzeit = millis()+n;
}

bool abgelaufenWecker(){ // true, wenn Zeit abgelaufen ist
  return millis()>=weckzeit;
}

bool gedruecktTaster(){ // true wenn Taster gedrückt wurde 1->0, entprellt
  static byte oldTaster=0;
  bool ausgabe = false;
  byte test = digitalRead(TASTER);
  if (oldTaster != test){ // hat sich was getan?
    delay(10); // 10 ms warten
    test=digitalRead(TASTER); // noch mal einlesen
    if (oldTaster != test){  // immer noch anders?
      ausgabe = oldTaster & !test; // fallende Flanke
      oldTaster = test;
    }
  }
  return ausgabe;
}

#ifdef DEBUG_ME 
  unsigned long maxSchwaetzoTime = 0;
  unsigned long tmp=0;
#endif

void starteSpiel(){
  schwaetzoL.an(); // Servosignal anschalten
  schwaetzoR.an();
#ifdef DEBUG_ME   
  maxSchwaetzoTime = 0;
#endif  
  stelleWecker(1000); // 1 Sekunde
}

byte lesePoti(){ // liest Potentiometer und setzt auf Servowerte um
  unsigned int p = analogRead(POTI); // POTI einlesen und 0..1023 auf 20..30 umsetzen
  return map(p,0,1023,100,200); // map(value, fromLow, fromHigh, toLow, toHigh)
}

String serIn; // für Serielle Eingabe
void loop() {
  static unsigned char naheZyklen=0;
  static byte i;
  switch (zustand){
    case RUHE_ENTRY:
      stelleWecker(400); // 0,4 Sekunden
      schwaetzoL.aus();
      schwaetzoR.aus();
      zustand=RUHE;
      break;
    case RUHE: // Warten auf Person, alle 0,4s Entfernung messen
      if (abgelaufenWecker()){ // wenn Zeit um ist
        if(messeEntfernung()<NAHE_ENTFERNUNG){
          zustand=NAHE;
          naheZyklen=0;
        }
        stelleWecker(400); // 0,4s
      }
      break;  
    case NAHE:
      if (abgelaufenWecker()){
        if(messeEntfernung()<NAHE_ENTFERNUNG){
          naheZyklen++;
        }
        else naheZyklen=0;
        stelleWecker(400);
      }
      if(naheZyklen>3){ // Person steht vor Automat
        starteSpiel();
        Serial.print(F("Spiele Stueck: "));
        Serial.print(stueck);
        myMP3.playFromMP3Folder(stueck);
        delay(700);
        i = myMP3.numTracksInFolder(0); // folder wird nicht beachtet, gibt Anzahl aus aktuellem Verzeichnis
          if (i>254){    // falls keine brauchbare Antwort
            delay(500);  // warten..
            Serial.print(F(", "));
            i = myMP3.numTracksInFolder(0); // nochmal versuchen
          }
          Serial.print(F(", Stücke im Verzeichnis: "));
          Serial.println(i);
        stueck++;
        if (stueck>i)stueck=1; // wenn alle Stücke durch sind wieder das erste Stück
        zustand=SPIELEN;
      }
      break;
    case SPIELEN:
#ifdef DEBUG_ME   
      tmp = micros();   // Microsekunden
#endif      
      schwaetzoL.go();  // Audio messen und auf Servo ausgeben
      schwaetzoR.go();
#ifdef DEBUG_ME       
      tmp = micros()-tmp; // Zeitdifferenz
      if (tmp>maxSchwaetzoTime) maxSchwaetzoTime=tmp;
#endif      
      if(abgelaufenWecker()){  // wenn Zeit um ist mal nach dem Rechten sehen
        /*if(!myMP3.isPlaying()){ // Stück zu ende? Braucht 64ms für Abfrage
          zustand=RUHE_ENTRY;
        }*/
        if(digitalRead(BUSY)){ // MP3 spielt? Low aktiv
          zustand=RUHE_ENTRY;
#ifdef DEBUG_ME           
          Serial.print(F("MaxSchwätzoTime: "));
          Serial.println(maxSchwaetzoTime);
#endif          
        }
        stelleWecker(1000); // 1 Sekunde
        einstellenLautstaerke();
      }
      break;
    case AUDIO_TEST:    // Schwätzofunktion visualisieren mit Seriellem Plotter
      schwaetzoL.go();  // Audio messen und auf Servo ausgeben
      schwaetzoR.go();
      if(schwaetzoL.getLastAudio()){ // wenn Audiosamples ausgewertet
        Serial.print("Lmax:");
        Serial.print(schwaetzoL.getMaxAudio());
        Serial.print(",");
        Serial.print("L:");
        Serial.print(schwaetzoL.getLastAudio());
        Serial.print(",");
        Serial.print("Rmax:");
        Serial.print(schwaetzoR.getMaxAudio());
        Serial.print(",");
        Serial.print("R:");
        Serial.println(schwaetzoR.getLastAudio());
      }
      if(abgelaufenWecker()){
        if(digitalRead(BUSY)){ // MP3 spielt? Low aktiv
          zustand=RUHE_ENTRY;
        }
        stelleWecker(1000); // 1 Sekunde
        einstellenLautstaerke();
      }
      break;
    case J_ANFANG:
      Serial.println(F(", bitte bei Erstjustage alle Servohebel von den Achsen abziehen! Dann Taste drücken."));
      zustand = J_SERVO_ABGEZOGEN;
    break;
    case J_SERVO_ABGEZOGEN:
      if(gedruecktTaster()){
        Serial.println(F("Beide Servohebel so aufsetzen, dass der jeweilige Mund halb geöffnet ist."));
        schwaetzoL.an(); // Servosignal anschalten
        schwaetzoR.an();
        schwaetzoL.moveServo(150); // Servo auf Mitte stellen
        schwaetzoR.moveServo(150); // Servo auf Mitte stellen
        zustand = J_SERVO_AUFSETZEN;
      }
      break;
    case J_SERVO_AUFSETZEN:
      if(gedruecktTaster()){
        Serial.println(F("Bitte Poti in die Mitte stellen. Dann Taste drücken."));
        zustand = J_POTI_MITTE;
      }
      break;
    case J_POTI_MITTE:
      if(gedruecktTaster()){
        i = lesePoti();
        if (i<140||i>160){
          Serial.print(F("Poti steht nicht in der Mitte: "));
          Serial.println(i);
        } else {
          Serial.println(F("Linken Servo in MundZu Position einstellen. Dann Taste drücken."));
          zustand = J_LINKS_ZU;
        }
      }
      break;  
    case J_LINKS_ZU:
      i = lesePoti();
      schwaetzoL.moveServo(i);
      if(gedruecktTaster()){
        Serial.print(F("ServoL zu: "));
        Serial.println(i);
        Serial.println(F("Linken Servo in MundOffen Position einstellen. Dann Taste drücken."));
        schwaetzoL.setMundZu(i);
        zustand = J_LINKS_AUF;
      }
      break;
    case J_LINKS_AUF:
      i = lesePoti();
      schwaetzoL.moveServo(i);
      if(gedruecktTaster()){
        Serial.print(F("ServoL auf: "));
        Serial.println(i);
        Serial.println(F("Bitte Poti in die Mitte stellen. Dann Taste drücken."));
        EEPROM.update(0,42);
        schwaetzoL.setMundOffen(i);
        schwaetzoL.schreibeEEPROM();
        zustand = J_POTI_MITTE2;
      }
      break;
      case J_POTI_MITTE2:
      if(gedruecktTaster()){
        i = lesePoti();
        if (i<140||i>160){
          Serial.print(F("Poti steht nicht in der Mitte: "));
          Serial.println(i);
        } else {
          Serial.println(F("Rechten Servo in MundZu Position einstellen. Dann Taste drücken."));
          zustand = J_RECHTS_ZU;
        }
      }
      break;
    case J_RECHTS_ZU:
      i = lesePoti();
      schwaetzoR.moveServo(i);
      if(gedruecktTaster()){
        Serial.print(F("ServoR zu: "));
        Serial.println(i);
        Serial.println(F("Rechten Servo in MundOffen Position einstellen. Dann Taste drücken."));
        schwaetzoR.setMundZu(i);
        zustand = J_RECHTS_AUF;
      }
      break;
    case J_RECHTS_AUF:
      i = lesePoti();
      schwaetzoR.moveServo(i);
      if(gedruecktTaster()){
        Serial.print(F("ServoR auf: "));
        Serial.println(i);
        Serial.println(F("Servos justiert."));
        schwaetzoR.setMundOffen(i);
        schwaetzoR.schreibeEEPROM();
        zustand = RUHE_ENTRY;
      }
      break;
  }
  if(Serial.available()){ // wenn Zeichen über Seriell Monitor gesendet wurden
    serIn = Serial.readStringUntil('\n'); // lese Daten in String ein
    Serial.print(serIn); // gib String aus
    switch (serIn[0]){
      case '?':
        Serial.println(F(" Hilfe"));
        Serial.println(F("p Pause"));
        Serial.println(F("s Stopp"));
        Serial.println(F("a1,1 Audiotest mit Ausgabe auf Serieller Plotter aus Verzeichnis 1, Stück 1"));
        Serial.println(F("v1,1 Spiele aus Verzeichnis 1, Stück 1"));
        Serial.println(F("j Servojustage"));
        break;
      case 'p':  // Pause
        Serial.println(F(" Pause"));
        myMP3.pause();
        break;
      case 's':  // Stopp
        Serial.println(F(" Stopp"));
        myMP3.stop();
        break;  
      case 'j':
        Serial.print(F(" Servojustage"));
        zustand = J_ANFANG;
        break;  
      case 'a':  // Audiotest
      case 'v':  // playFolderTrack
        byte folder=0,track=0;
        i=1;
        while (i<serIn.length() && (serIn[i]<'0'||serIn[i]>'9')) i++; // Nichtdigits überspringen
        while (i<serIn.length() && serIn[i]>='0'&& serIn[i]<='9'){  // Digits einlesen
          folder = folder*10+ (serIn[i]-'0');
          i++;
        }
        while (i<serIn.length() && (serIn[i]<'0'||serIn[i]>'9')) i++; // Nichtdigits überspringen
        while (i<serIn.length() && serIn[i]>='0'&&serIn[i]<='9'){  // Digits einlesen
          track = track*10+ (serIn[i]-'0');
          i++;
        }
        if(folder>=0 && track>0){
          Serial.print(F("Spiele aus Verzeichnis "));
          Serial.print(folder);
          Serial.print(F(" Stück "));
          Serial.print(track);
          if(folder==0) myMP3.playFromMP3Folder(track); // MP3-Verzeichnis
          else myMP3.playFolder(folder,track);
          delay(1000); // warten bis verarbeitet
          i = myMP3.numTracksInFolder(folder); // folder wird nicht beachtet, gibt Anzahl aus aktuellem Verzeichnis
          if (i>254){    // falls keine brauchbare Antwort
            delay(500);  // warten..
            Serial.print(F(", "));
            i = myMP3.numTracksInFolder(folder); // nochmal versuchen
          }
          Serial.print(F(", Stücke im Verzeichnis: "));
          Serial.println(i);
          if(serIn[0]=='a') zustand = AUDIO_TEST;
          else zustand = SPIELEN;
          starteSpiel();
        } else {
          Serial.print(F("Fehler beim Parsen: "));
          Serial.println(serIn);
        }
        break;
    }
  }
}