Musik machen mit dem Arduino Due
Viele Leser werden wahrscheinlich bereits den Arduino Uno kennen und unter Umständen schon einmal mit diesem gearbeitet haben. Dies ist sogar wahrscheinlich, denn für den Arduino Uno gibt es unzählige Projekte und auch Zusatzhardware. Der Phantasie werden hier in der Tat kaum Grenzen gesetzt. Vielleicht fragen auch Sie sich nun, warum Sie dann den sehr teuren Arduino Due benutzen sollten, der stolze 70 Euro kostet.
Die Antwort lautet, dass man eine Sache nicht mit dem Arduino Uno tun kann: Musik machen. Der Arduino Uno hat nämlich keine DAC-Bausteine. DAC ist die Abkürzung für Digital to Analog Converter, also ein Baustein, der einen ganzzahligen Wert in eine Spannung umwandelt. Wenn Sie eine Spannung dann an einen Audio-Ausgang anlegen, können Sie diese Spannung als Ton hören. Die Voraussetzung ist, dass sich die am DAC ausgegebene Spannung schnell genug ändert. Übrigens: Der Raspberry Pi Pico, über den ich bereits einen Artikel verfasst habe, hat keinen DAC-Baustein, auch wenn diese Behauptung in Foren immer wieder auftaucht.
Bei den ganzen Tricks, die z. B. den 4. Pin des Raspberry Pi Pico als Audio-Ausgang benutzen, handelt es sich immer um Workarounds und keine echten DAC-Ausgaben. Der Trick hier: Rechteck-Pulse werden so schnell ausgegeben, dass sich die Signale an dem Ausgangs-Pin verwischen und Sie deswegen auch Spannungen zwischen 0 und 3,3V erzeugen können, und nicht nur Lo- und Hi-Pegel.
Ich rate Ihnen an dieser Stelle davon ab, diese Tricks zu verwenden, denn die GPIO-Pins des Raspberry Pi Pico sind einfach nicht für sehr schnelle Pulsfolgen ausgelegt. Vielleicht haben Sie an dieser Stelle noch im Hinterkopf, dass der ESP32-Mikrocontroller DAC-Pins besitzt. Sie haben Recht: Der ESP32 besitzt sogar zwei davon, aber die DACs des ESP32 haben nur eine Breite von 8 Bits. Wenn Sie also mehrere Stimmen zusammenmischen wollen, haben Sie schon ein Problem mit der Klangqualität.
Der Arduino Due hat diese Probleme nicht, denn die zwei DAC-Bausteine des Due haben eine Breite von 12 Bits und sind auch sehr rauscharm. Aber auch die anderen Werte des Arduino Due können sich sehen lassen:
- 32-Bit-ARM-Cortex-Mikroprozessor der M0-Reihe mit 84 MHz Taktfrequenz (leider keine 2 Kerne)
- 512 kB RAM, hiervon sind 96 kB frei für Daten nutzbar, der Rest ist Programmspeicher (mit Tricks kann jedoch auch mehr RAM für Daten benutzt werden)
- Erweiterungsspeicher kann zusätzlich angeschlossen werden, hierfür gibt es spezielle Platinen zum Aufstecken
- Auch für SD-Karten gibt es inzwischen spezielle Aufsteck-Platinen
- 3 serielle Schnittstellen (leider 3,3V-Pegel)
- 1 SPI-Schnittstelle
- 1 I2C-Schnittstelle
- 1 USB 1.1-Schnittstelle für Maus ODER Tastatur (leider kein Hub nutzbar)
- 2 DAC-Ausgänge mit 12 Bit Breite (Ausgabespannung 0 bis 3,3V)
Mit welchen Apps kann der Arduino Due programmiert werden?
Vielleicht können Sie bereits etwas Python, weil Sie diese Programmiersprache schon für andere Mikrocontroller-Projekte verwendet haben. Der inzwischen sehr beliebte Raspberry Pi Pico wird z. B. oft mit Python programmiert. Python gibt es inzwischen auch für den Arduino Due, aber für Audio-Ausgaben ist Python schlicht zu langsam.
Deshalb werde ich für die Musik-Programmierung C benutzen, zusammen mit der aktuellen Arduino-IDE, die natürlich auch den Due vollständig unterstützt. Wie Sie die Arduino-IDE installieren, will ich hier nicht noch einmal detailliert beschreiben, denn zu diesem Thema gibt es bereits zahlreiche Foren und Artikel. Die aktuelle Arduino-IDE finden Sie auf der Webseite [1]. Alle Versionen ab 1.6.x unterstützen von Haus aus den Arduino Due.
Programmierung des Due mit der Arduino-IDE
Die Arduino-IDE ist, wie der Name schon andeutet, dafür gedacht, C-Programme auf Arduino-Mikrocontrollern auszuführen. C-Programme werden stets kompiliert, d. h. in Maschinencode übersetzt. Diese Tatsache macht die Arduino-Programme so schnell und leistungsfähig. Da es auch von dem Arduino mit der Zeit eine unüberschaubare Anzahl von Varianten gab, hatte die Entwicklergemeinde irgendwann mit großen Problemen zu kämpfen und einige Varianten (wie z. B. der Yun) liefen auch nie richtig stabil.
Die Lösung für diese Probleme war erstens, offene Standards zu entwickeln, und zweitens, möglichst viele Mikrocontroller durch die IDE zu unterstützen – auch Mikrocontroller fremder Hersteller. Die Rechnung ging auf und mittlerweile werden bis zu 100 verschiedene Mikrocontroller unterstützt, darunter ist inzwischen auch der ESP32-Mikrocontroller für IoT-Anwendungen und der neue Raspberry Pi Pico.
Installation der Arduino-IDE
Auch bei der Installation der IDE hat sich vieles vereinfacht. Um die IDE aufzuspielen, rufen Sie als erstes die Webseite auf [1]. Anschließend wählen Sie Software aus. Da die IDE in Java programmiert wurde, unterstützt die Version für Windows ab 7 auch Windows 8, 10 und 11. Für Linux gibt es die IDE zwar inzwischen auch über Standard-Paketmanager wie APT oder RPM, auf anderen Plattformen als dem Raspberry Pi ist die IDE jedoch nur selten von Haus aus aktuell. In diesem Fall müssen Sie sich das entsprechende Archiv in Form einer ZIP-Datei per Hand herunterladen und installieren. Wie dies geht, erfahren Sie in den zahlreichen How-tos, die z. B. in Form einer Readme-Datei in dem entsprechenden Archiv enthalten sind. Lesen Sie sich auf jeden Fall die How-tos durch, denn die Installation kann sich von Fall zu Fall unterscheiden. Unter Windows ist die Sache deutlich einfacher, denn dort gibt es nur eine .exe-Datei, die Sie einfach ausführen müssen, um die Installation zu starten.
Wenn Sie die entsprechende Version ausgewählt haben, erscheint unter Umständen noch ein Spendenaufruf der Arduino-Community, mit der Bitte, einen bestimmten Betrag zu spenden. Diese Spende ist freiwillig, Sie können die Arduino-IDE also auch herunterladen, ohne etwas zu spenden. Klicken Sie hierzu auf just download. Wenn Sie etwas spenden wollen (was sehr viele Benutzer in der Tat tun), dann klicken Sie contribute and download.
Die Installation selbst ist sehr einfach. Zuerst müssen Sie die GPL-Lizenz akzeptieren, die oft für freie Open-Source-Software verwendet wird. Anschließend wird die Installation gestartet, die im Allgemeinen einfach durchläuft und sich am Ende mit Installation completed meldet. Es wird auch immer ein Desktop-Symbol für die Arduino-IDE angelegt. Wenn dies nicht geschieht, dann haben Sie ein ernstes Problem mit Ihrem Gerät oder Ihrer Systemkonfiguration, die aber dieser Artikel nicht lösen kann. Ich setze nun voraus, dass dies nicht der Fall ist und dass Sie die Software nun durch einen Klick auf das entsprechende Symbol starten können.
Schließen Sie nun Ihren Arduino Due an einen freien USB-Port an und warten einige Zeit, bis Windows die entsprechenden Treiber installiert hat (unter Umständen müssen Sie die Installation auch separat bestätigen, beim neuen Windows 11 ist dies stets der Fall). Starten Sie nun nach erfolgreicher Treiber-Installation die Arduino-IDE und installieren zunächst einmal den Board-Treiber für den Arduino Due. Dies ist nicht so schwer, denn im Endeffekt müssen Sie nur den richtigen Menüeintrag auswählen: Wählen Sie im Menü Werkzeuge den Eintrag Board aus und anschließend das Untermenü Boardverwalter.
Es öffnet sich nun ein weiteres Fenster mit einer Liste aller von der Arduino-IDE unterstützten Boards. Um den Treiber für den Arduino Due zu suchen, müssen Sie unter Umständen etwas scrollen, denn meistens befindet sich der Treiber am unteren Ende der Liste. Die richtige Version hat die Bezeichnung ARM SAM Boards und enthält unter anderem auch den Arduino Due. Für die Installation des richtigen Treibers genügt ein Klick auf Installieren. Wenn der korrekte Board-Treiber installiert wurde, kann nun ein neues C-Programm geschrieben werden.
Starten der Arduino-IDE
Beim Start der Arduino-IDE wird schon automatisch ein Programmgerüst angelegt, das die Funktionen setup()und loop() enthält. Die Funktion setup() wird genau einmal aufgerufen, die Funktion loop() immer wieder neu in einer Endlosschleife. Dies ist deshalb so, weil Mikrocontroller oft Bestandteil von anderen elektronischen Geräten sind, die z. B. immer wieder den Status der Bedientasten abfragen müssen. Das Setup wird aber immer nur beim Start des Gerätes aufgerufen.
Schließen Sie nun Ihren Arduino Due anhand der Abb. 1 an den Line-In-Eingang Ihres Monitors oder Ihrer Stereo-Anlage an:
Ältere Arduino Due bezeichnen die DAC-Pins noch mit DAC0 und DAC1, neuere mit DAC2 und DAC1 (wobei hier der alte DAC0-Pin dem neueren DAC1-Pin entspricht und die DAC-Pins beim neuen Arduino Due vertauscht wurden).
Das erstes Programmbeispiel – einen hörbaren Ton ausgeben
Wenn Ihr Arduino Due läuft und korrekt erkannt wurde (das sehen Sie daran, dass diesem eine korrekte Portnummer zugeordnet wurde), können Sie Programme auf diesen hochladen. Um Programme (auch Sketche genannt) hochzuladen, müssen Sie den entsprechenden Port aus dem Menü Port in der Arduino IDE auswählen. Ein typischer Eintrag ist z. B. COM3 Arduiono Due Programming Port. Die Benutzung von Programming Port ist deshalb wichtig, weil es auch noch einen zweiten Port gibt, über den Sie z. B. eine Maus anschließen, aber keine Sketche hochladen können. Natürlich gibt der Arduino auf diese Weise noch lange keinen Ton aus. Dies ist aber auch klar: Sie haben Ihrem Mikrocontroller ja noch nicht mitgeteilt, welchen der zwei DAC-Pins Sie benutzen wollen. Außerdem können Sie frei wählen, wie viele Bits Ihre DAC-Pins nutzen sollen. Zu diesem Zweck verwenden Sie die Funktion analogWriteResolution() wie folgt:
analogWriteResolution(12);
Wenn Sie die obere Zeile in die zunächst leere setup()-Funktion eintragen, wählen Sie eine Auflösung von 12 Bit aus, das heißt, Sie können eine Zahl zwischen 0 und 4095 an einen der DAC-Pins schicken. Der Wert 0 bedeutet eine Ausgabespannung von 0V, ein Wert von 4095 eine Ausgabespannung von 3,3V. Angenommen, in der Variablen Out steht der Wert, den Sie an DAC-Pin 0 schicken wollen. In diesem Fall verwenden Sie die Funktion analogWrite() in der folgenden Form:
analogWrite(DAC0,Out);
Einen Ton haben Sie aber auch hiermit noch nicht erzeugt, denn konstante Spannungen am Audio-Ausgang kann Ihr Ohr nicht in einen Ton umwandeln. Ein Ton entsteht erst, wenn Sie ein Signal an Ihrem DAC-Pin ausgeben. Ein Signal ist definiert als die Änderung der Spannung über die Zeit und erst dieses Signal können Sie wahrnehmen (natürlich stets über den Signalweg Spannung → DAC → Lautsprecher → Ohr → Gehirn).
Am besten können Sie Töne dann hören, wenn Ihr Audio-Signal periodisch ist. Periodisch bedeutet, dass sich das Signal dauernd wiederholt. Die Rate der Wiederholung nennt man Frequenz, die in Hz (Hertz) gemessen wird. Ein Hz entspricht einer Wiederholung pro Sekunde, ab 1000 Wiederholungen pro Sekunde benutzt man oft die Einheit Kilohertz (1 KHz=1000 Hz). Die Periode selbst (also der Teil, der dauernd wiederholt wird) wird als Wellenform bezeichnet.
Sie müssen also, um einen hörbaren Ton zu erzeugen, eine periodische oder sich ändernde Zahlenfolge an einen DAC-Pin senden. Dies allein reicht aber nicht aus, denn Sie müssen diese Zahlenfolge auch mit einer bestimmten Geschwindigkeit senden. Nehmen wir als Beispiel nun die folgende Zahlenfolge:
0, 1, 2, 3, …,4095, 0, 1, 2, …
Diese Zahlenfolge könnte z. B. durch die folgende Programmzeile entstehen:
Out=(Out+1)%4096;
Der Modulo-Operator % sorgt dafür, dass Out den Wert 4095 nicht überschreiten kann. Allerdings führt hier eine zusätzliche Zeile wie diese nicht wirklich zum Ziel:
analogWrite(DAC0,Out);
Ein Ton wird hier bestimmt ausgegeben, weil Sie eine sogenannte Sägezahn-Welle erzeugen, die auf einem Oszilloskop wie ein Sägeblatt aussieht. Diese Sägezahn-Welle ist ein periodisches Signal, das Sie auch hören können. Allerdings kann nicht einmal ich sagen, wie hoch Ihr Ton wirklich ist. Ist er eher tief oder erzeugen Sie an dieser Stelle einen schrillen Pfeifton, den Sie kaum ertragen können?
Kurz: Die Höhe eines Tons hängt von der Frequenz ab – je höher die Frequenz in Hz, desto höher der Ton. Sie müssen also Ihre Sägezahn-Wellenform mit einer bestimmten Geschwindigkeit ausgeben. Hierzu benötigen Sie eine sehr präzise Uhr. Glücklicherweise besitzt jeder Arduino (auch der Due) eine solche präzise Uhr, mit der Sie sogar Mikrosekunden zählen können. Die Abfrage der internen Uhr ist sehr einfach und erfolgt mit folgender Funktion:
unsigned long int Time=micros();
Mit der Funktion micros() können Sie nun auf die folgende Weise eine Sägezahnfunktion mit einer konstanten Frequenz erzeugen:
// ErsteSchritte.ino
void setup()
{
analogWriteResolution(12); // DACs auf 12-Bit-Werte einstellen
}
void loop()
{
unsigned long int Out=micros()%4095; // Die Modulo-Funktion erzeugt eine Sägezahn-Wellenform
analogWrite(DAC0,Out);
}
Töne mit dem Arduino ausgeben
Bevor Sie den Sketch ErsteSchritte.ino ausführen, verbinden Sie bitte Ihren Arduino anhand des folgenden Schaubildes mit dem Audio-Eingang Ihrer Stereoanlage oder Ihres PC-Monitors. Benutzen Sie nur abgeschirmte Klinkenkabel und hochwertige Stecker, da es sonst zum Rauschen kommen kann. Erst nachdem Sie alles korrekt verbunden und getestet haben, kompilieren Sie Ihren Sketch.
Die Frequenz des ausgegebenen Tones ist nun 1.000.000/4.095=244,14 Hz. Dies entspricht in etwa dem Ton D#3, der Kammerton A4 hat 440 Hz. Ein grundlegender Nachteil des einfachen Einführungsbeispiels ist also, dass Sie keine bestimmte Frequenz einstellen können, um einen ganz bestimmten Ton (z. B. C3) auszugeben. Leider ist die Skalierung Ihrer Ausgabefrequenz auf eine ganz bestimmte Note nicht trivial. Ihr Ohr hört z. B. nicht linear. Dies bedeutet, dass sich Ihr Ton bei jeder Verdopplung der Frequenz um eine Oktave erhöht und bei Halbierung der Frequenz um eine Oktave erniedrigt. Ferner wird die nicht lineare Oktave noch einmal in 12 Untereinheiten unterteilt, nämlich folgende Grundtöne:
A, A#, C, C#, D, D#, E, F, F#, G, G#
Ab dem 12. Ton nach dem ersten Grundton A landen Sie dann wieder beim Ton A, nur eine Oktave höher. Sie definieren also 12 Töne pro Oktave. Den tiefsten Ton, den Sie auf einem Klavier spielen können ist der Ton A0 mit 27,5 Hz=440/2/2/2/2 Hz. Die Formel, um die Frequenz einer ganz bestimmten Note zu bestimmen, ist somit (^=mathematisches "hoch"):
F=27,5*2^I/12
I ist hier die Anzahl der Halbtonschritte, die Sie vom Ton A0 aus nach oben gehen müssen, um zu dem gesuchten Ton zu gelangen. Im Fall von C3 sind dies 39 Halbtonschritte, die Frequenz von C3 ist also:
F=27,5*239/12=261,62 Hz
Dies bedeutet nun folgendes: Sie müssen Ihren Original-Rückgabewert, den micros() zurückliefert, zunächst mit dem Faktor 261,62/244,14=1,072 multiplizieren (aber als Zahl vom Typ double betrachtet), anschließend in eine Zahl vom Typ unsigned long int wandeln und zum Schluss noch Modulo 4096 nehmen. Dies führt zu dem folgenden Sketch:
// C3.ino
double F; // F darf keine ganze Zahl sein
double Fakt; // Multiplikationsfaktor
void setup()
{
analogWriteResolution(12); // DACs auf 12-Bit-Werte einstellen
F=261.62; // Gewünschte Ausgabefrequenz
Fakt=F/244.14; // Dazugehöriger Faktor für die Uhr
}
void loop()
{
double OutD=(double)(micros()); // zunächst 244,14 Hz,…
OutD*=Fakt; // …nun 261,62 Hz (C3)
unsigned long int Out=((unsigned long int)(micros())%4095; // der Operator % funktioniert nicht mit double
analogWrite(DAC0,Out);
}
Der Arduino Due kommt erstaunlich gut mit der Berechnung von Fließkommazahlen zurecht, die Geschwindigkeitseinbußen durch einen Frequenzfaktor vom Typ double sind hier minimal. Wenn Sie später Samples (die z. B. als Rohdaten in einem Array gespeichert werden) mit einer bestimmten Frequenz abspielen wollen, kommen Sie auch um Fließkommazahlen vom Typ double nicht herum. Allerdings gibt es für einfache Wellenformen Tricks, um Ihre Programme zu beschleunigen. Sie können z. B. einen separaten 32-Bit-Wert Val benutzen, der einmal pro Mikrosekunde zu Ihrem Zähler addiert wird. Dies entspricht der folgenden Programmzeile:
Count+=Val*micros();
Nun bestimmen Sie mittels Val, wie schnell Ihre Zahlenfolge ansteigt und so z. B. die Frequenz der Sägezahn-Wellenform. Allerdings erhalten Sie auf diese einfache Weise schon mit V=2 eine Frequenz von 244,14*2=488,28 Hz. Wie Sie sehen, erreichen Sie hier nur eine nur sehr grobkörnige Skalierung. Diese Grobkörnigkeit lässt sich aber mit einem weiteren Trick beheben: Sie benutzen für Ihre wirklich als Wellenform ausgegebene Zahlenfolge nur die oberen Bits eines 24 Bit breiten Zählers. Dies erreichen Sie wie folgt:
Count+=Val*micros(); // Zähler erhöhen
Count&=0xffffff; // Oberste 8 Bits durch eine AND-Maske ausmaskieren
Out=Count>>12; // >>=“shift right“, was einer Division durch 4096 entspricht
Ihre Ausgabefrequenz entspricht durch den hier angewendeten Trick in etwa 1/16 Hz (genauer:1.000.000/16.777.216 Hz) pro Zählerschritt, wenn Sie also pro Mikrosekunde den Wert Val=160 zu Cnt addieren, erhalten Sie eine Ausgabefrequenz von 10 Hz. Wenn Sie es nun sehr genau nehmen, dann setzen Sie 0,0597 Hz pro Zählerschritt als Frequenzfaktor ein, also 1.000.000/16.777.216. Allerdings ist der Fehler hier nur minimal gegenüber dem Faktor 1/16=0,0625 und der Unterschied entspricht gerade mal einem Viertel Halbtonschritt für die 6. Oktave. Dieser Unterschied liegt sehr nahe an der unteren Wahrnehmungsschwelle, allerdings mag es professionelle Musiker geben, die hier eine Unstimmigkeit feststellen.
Übrigens: Ich habe den Trick mit dem Zähler Cnt und der Bitverschiebung nicht selbst erfunden: Der legendäre SID-Soundchip im C64 arbeitet auf dieselbe Weise. Sie fragen sich vielleicht, wie ich die Interna des SID herausgefunden habe. Die Antwort: Mit einem Original-C64, vielen Messungen mit dem Oszilloskop, jeder Menge Geduld und zahlreichen Internet-Recherchen.
Abspielen eines Songs mit einem Sample-Player
Es soll nun ein Sketch erstellt werden, der den Walzer "An der schönen blauen Donau" auf dem Arduino abspielt. Weil in einem relativ kurzen Artikel nicht so viel Platz für ausgiebige Programm-Listings ist, beschränke ich mich an dieser Stelle auf eine Mono-Ausgabe und benutze nur den DAC-Pin 0. Ferner benutze ich nur ein Instrument, nämlich eine Flöte. Die Noten sind schon vorhanden und so wird der Song selbst als vordefiniertes Array angegeben. Das Instrument selbst – die Flöte – liegt als digitalisiertes Signal vor, also als Sample.
Was sind Samples?
Ein Sample ist ein digitalisiertes Audio-Signal. Folglich enthält ein Sample einen bestimmten Ton in Form von Zahlen. Leider gibt es verschiedene Audio-Formate, d. h. , Sie können die Zahlen eines Samples, die z. B. in dem Array S stehen, oft nicht 1:1 an den DAC Ihres Arduinos weiterreichen. So verwendet z. B. das bekannte WAV-Audioformat von Windows vorzeichenbehaftete 16-Bit-Zahlen, die Sie zwar sehr einfach zusammenmischen können, mit denen Ihr DAC aber nichts anfangen kann. Überdies verwenden die meisten Audio-Formate Bitbreiten, die ein Vielfaches von 8 sind, deshalb gibt es auch kein Audio-Format für die 12 Bit breiten DACs des Arduino Due. Wenn Sie also Samples mit Ihrem Arduino Due abspielen und zusammenmischen wollen, müssen Sie ein Audio-Format wählen, mit dem man gut arbeiten kann.
Zum Glück gibt es dieses Format in Form des RAW-Formates. RAW bedeutet "roh", d. h. Sie speichern die Audio-Daten Ihrer Soundkarte oder Ihres Handys ohne zusätzlichen Datei-Header ab. Das Format, das sich für den Arduino am besten eignet, ist das Format "8 bit mono signed". Dies bedeutet, dass Ihr Sample in Form von einzelnen Bytes gespeichert wird, die auch Vorzeichen enthalten können. In C entspricht dies dem Datentyp signed char. Es gibt zahlreiche kostenlose Programme, die Audio-Dateien im RAW-Format abspeichern können, z. B. das sehr beliebte Audacity. Audacity selbst kann sehr viele verschiedene Audio-Formate lesen, darunter auch MP3-Dateien oder Dateien, die Sie mit Ihrem Smartphone aufgenommen haben. Aber auch mit dem kostenlosen Kommandozeilen-Tool LAME können Sie verschiedene Formate ineinander umwandeln. Wichtig für den Arduino ist nur, dass Sie am Ende eine Datei haben, die Daten im Format "8 bit mono signed", am besten mit 11.025 Samples pro Sekunde, beinhaltet. Die Audio-Qualität entspricht hier in etwa dem des Amiga 500.
Datei auf den Arduino Due übertragen
Wie bekommen Sie die Audio-Datei aber nun auf Ihren Arduino Due? Eine Möglichkeit ist der Erwerb eines SD-Karten-Lesers und der zusätzlichen Verwendung einer Bibliothek, mit der Sie anschließend die Daten z. B. in ein Array einlesen können. Solche sogenannten SD-Card-Shields bekommen Sie im Internet sehr günstig in Online-Shops wie z. B. Adafruit, aber auch bei einem Elektronik-Versandhandel wie Völkner, Conrad und Co. Das Angebot in diesem Sektor ist quasi unüberschaubar, aber meistens tut es wirklich schon ein günstiges Shield für 10 bis 20 Euro.
Eine andere Möglichkeit ist, eine Include-Datei zu erstellen, die die Samples in der folgenden Form enthält:
signed char Sample[]=
{
0x00,0x3f,0x14,0x08,…
};
Allerdings müssen Sie in diesem Fall die Daten irgendwie in Textform umwandeln. Wenn Sie einen zusätzlichen C-Compiler wie z. B. Pelles C oder den GCC-Compiler für Ihren PC zur Verfügung haben, können Sie das folgende Programm benutzen, das die Sample-Datei einliest und die Daten anschließend in der richtigen Form in der Konsole bzw. Eingabeaufforderung ausgibt:
// Hexout.c
#include<stdio.h>
#include<conio.h>
#include<string.h>
void main()
{
unsigned int CHS=0;
bool TON=false;
unsigned char Filename[1024];
unsigned char C=0;
long int i=0;
printf("Dateiname:"); scanf("%s",Filename); printf("\n***\n");
FILE *OUTFILE=fopen(Filename,"rb");
while (!feof(OUTFILE))
{
fread(&C,1,1,OUTFILE);
if (!feof(OUTFILE))
{
printf("0x%02x,",C&0xff);
i++;
}
if (i==32) { printf("\n"); i=0; }
}
printf("\n%ld Bytes\n",j);
fclose(OUTFILE);
getch();
}
Wenn Sie nun die Konsole (unter Linux) bzw. die Eingabeaufforderung (unter Windows 11) so einstellen, dass sich die Konsole sämtliche ausgegebenen Zeilen merkt, dann können Sie die Konsolen-Ausgabe mit der Maus (auf dem Linux-Desktop) bzw. dem Befehl Alles markieren → Kopieren (in der Windows 11 Eingabeaufforderung) in die Zwischenablage kopieren und anschließend in Ihren Arduino-Sketch einfügen.
Samples abspielen
Wir wissen bereits, dass ein Sample ein digitalisiertes Audio-Signal ist. Wie man Samples in ein Array konvertieren kann, wissen wir auch bereits. Nun muss dieser Sample, der z. B. einen Flötenklang enthält, abgespielt werden. Dies ist trivial, denn für das Abspielen eines Samples der Länge L mit der Samplerate R im Array S benötigen Sie lediglich die folgende Loop-Funktion:
void loop()
{
unsigned long int PosL;
double PosD=(double)(micros())*SF; // SF=sample factor
PosL=(unsigned long int)(PosD); // Der Array-Index muss
eine ganze Zahl sein
if (PosL<L)
{
analogWrite(DAC0,(int)(Sample[PosL])+2048); // 12-Bit-DAC
mit Nullpunkt 2048
}
else
{
analogWrite(DAC0,0); // DAC0 bei Ende des Sounds abschalten
}
}
Der Faktor SF berechnet sich wie folgt:
SF=(double)(R)/(doulbe)(1000000); // R=11025
In der Tat wird Ihr Sound korrekt abgespielt, aber leider nur einmal. Wenn nämlich PosL>=L ist, wird der DAC-Pin abgeschaltet, und Ihr Arduino verstummt. Sie können auch in diesem Fall Ihren Ton nicht wieder neu anstoßen, weil micros() im Laufe der Zeit immer größere Werte liefert (es sei denn, Sie drücken den Reset-Knopf).
Ein noch größeres Problem ist aber, dass Sie auch ein periodisches Signal verwenden könnten. Dies müssen Sie dann auch so abspielen, dass es ständig wiederholt wird, und zwar ohne Unterbrechung. Hier ist die Modulo-Funktion und die Verwendung eines Offsets die Lösung:
unsigned long int TOffs=0;
void loop()
{
unsigned long int PosL;
double PosD=(double)(micros()-TOffs)*SF; // SF=sample factor
PosL=(unsigned long int)(PosD); // Der Array-Index und der Zeit-Offset TOffs muss eine ganze Zahl sein
PosL=PosL%L; // Die Modulo-Funktion bewirkt eine ständige Wiederholung des Samples mit der Länge L
analogWrite(DAC0,(int)(Sample[PosL])+2048); // 12-Bit-DAC mit Nullpunkt 2048
}
TOffs ist am Anfang 0. Wenn Sie nun den Ton neu anstoßen wollen, dann müssen Sie TOffs mit der folgenden Formel neu berechnen:
TOffs=micros();
TOffs markiert nun den Zeitpunkt, an dem Sie den Ton neu angestoßen haben, und da die interne Uhr immer weiterläuft, liefert die Zeile nun auch wieder den Wert PosD=0.0 zurück:
double PosD=(double)(micros()-TOffs)*SF; // SF=sample factor
Da Sie an der Modulo-Funktion nichts ändern, wird Ihr Ton auch weiterhin periodisch abgespielt. Leider können Sie den Ton aber nur anstoßen und nicht stoppen. An dieser Stelle können Sie zwei Dinge tun: Entweder Sie verwenden eine zusätzliche Variable vom Typ bool, die den Ton einschaltet, wenn sie true ist, oder Sie verwenden einen Hüllkurvengenerator.
Sample, Samples, Sample-Rate und Sample-Player
Der Begriff Sample ist leider nicht eindeutig definiert, weswegen es hier auch oft zu einer gewissen Verwirrung kommen kann. Erst einmal ist ein Sample nur ein Signal, also ein kurzer Ton, der eben digitalisiert – also in Form von Daten – vorliegt. Allerdings wird im Zusammenhang mit Samples oft das Wort Sample-Rate benutzt, das eigentlich die Geschwindigkeit beschreibt, mit der ein Ton abgespielt wird – in diesem Fall ist also ein Sample ein einzelner Messwert.
Der Plural von Sample ist Samples, und auch hier kommt es oft zu Mehrdeutigkeiten. Z. B. ist ein Sample-Player ein Programm, das einzelne Samples abspielen kann, in diesem Fall ist ein Sample also wieder ein digitalisierter Instrumentenklang, der auch in der Tonhöhe skaliert und mit anderen Samples gemischt werden kann.
Attack, Decay, Sustain, Realease
Zusätzlich zu den reinen Daten des zuvor digitalisierten Klangs kann ein Instrumentenklang eine Hüllkurve besitzen. Innerhalb einer Hüllkurve wird der Klang nicht mit einer gleichmäßigen Lautstärke abgespielt, sondern schwillt an (Attack), schwillt wieder bis zu einer bestimmten Lautstärke ab (Decay), wird eventuell gehalten (Sustain) und klingt anschließend aus (Release). Vor allem, wenn ein Instrumenten-Klang periodisch abgespielt wird (z. B. die Schwingung einer Gitarrensaite), besitzt dieser fast immer eine Hüllkurve.
Ein Hüllkurvengenerator ist nun die entsprechende Funktion (z. B. in C), die den Hüllkurvengenerator in Form einer Zustandsmaschine in die Praxis umsetzt. Dabei muss der Hüllkurvengenerator nicht alle Parameter umsetzen und kann der Einfachheit halber auch nur einen Attack- und einen Realease-Zyklus haben.
Noten auf dem Arduino DUE abspielen
Zunächst benötigen Sie ein Instrument. Nehmen wir eine einfache C-Flöte, die durch folgende Sample-Daten (8 Bit mono signed, 11025 SPS) definiert ist:
signed char Inst[]=
{
0x0e,0x3a,0x5f,0x77,0x7d,0x70,0x53,0x24,0xf2,0xc1,0xae,0xb7,0xd6,
0xfd,0x13,0x18,0x07,0xea,0xca,0xaf,0x9e,0x9d,0xb1,0xce,0xf4,0x1c,
0x46,0x65,0x78,0x78,0x64,0x3e,0x11,0xe1,0xba,0xad,0xc1,0xe6,0x06,
0x17,0x11,0xfc,0xe0,0xbd,0xa3,0x96,0x9e,0xb8,0xdd,0x03,0x2e,0x54,
0x71,0x7e,0x73,0x5b,0x33,0x01,0xd3,0xb0,0xb2,0xcb,0xef,0x0d,0x1a,
0x11,0xf4,0xd4,0xb3,0x9b,0x94,0xa1,0xbf,0xe9,0x14,0x40,0x62,0x76,
0x7e,0x72,0x4f,0x1f,0xf3,0xc7,0xae,0xb7,0xd5,0xfa,0x15,0x1a,0x04,
0xe7,0xc3,0xa8,0x95,0x92,0xaa,0xcc,0xf8,0x22,0x4a,0x69,0x7c,0x7a,
0x63,0x41,0x10,0xe3,0xb8,0xab,0xc0,0xe4,0x01,0x16,0x0f,0xf9,0xd8,
0xb6,0x9c,0x92,0x9b,0xb6,0xda,0x03,0x30,0x57,0x70,0x7d,0x76,0x59,
0x2f,0x05,0xd8,0xb4,0xb0,0xca,0xeb,0x0a,0x17,0x0b,0xf1,0xcb,0xae,
0x98,0x91,0x9e,0xc0,0xe6,0x14,0x3f,0x61,0x77,0x7d,0x6f,0x50,0x24,
0xf5,0xca,0xaf,0xb7,0xd4,0xf7,0x14,0x15,0x04,0xe6,0xc1,0xa4,0x93,
0x93,0xa8,0xca,0xf6,0x22,0x4a,0x6a,0x7a,0x79,0x67,0x43,0x18,0xe5,
0xb9,0xb0,0xc2,0xe1,0x02,0x16,0x13,0xfc,0xd8,0xb6,0x99,0x8e,0x95,
0xb1,0xd8,0x05,0x31,0x58,0x71,0x7c,0x75,0x5c,0x35,0x06,0xd5,0xb5,
0xb1,0xcb,0xf0,0x0d,0x19,0x0d,0xf0,0xcf,0xad,0x96,0x8e,0x9e,0xbe,
0xe8,0x14,0x41,0x63,0x7a,0x7d,0x6f,0x51,0x25,0xf8,0xca,0xb1,0xba,
0xda,0xfe,0x16,0x1a,0x0a,0xe4,0xc1,0xa3,0x91,0x92,0xa8,0xcb,0xf6,
0x26,0x51,0x6c,0x7c,0x79,0x68,0x43,0x15,0xe8,0xc1,0xb8,0xc6,0xe9,
0x08,0x1b,0x14,0xfd,0xd9,0xb6,0x9a,0x90,0x96,0xb3,0xd9
};
Nun besteht selbst ein einfaches Musikstück aus einzelnen Noten. Diese Feststellung ist trivial aber entscheidend für die nächsten Schritte. Sie haben nämlich bis jetzt noch keine Musik gemacht, weil Sie bis jetzt gar keine Noten benutzt haben. Die Klänge, die Sie bis jetzt abgespielt haben, waren statisch.
Um mehrere Noten nacheinander abzuspielen, müssen die Noten, die Sie aus der Musik kennen, erst einmal in Zahlen umgewandelt werden. Diese Zahlen landen anschließend im Array Song. Ein sehr einfaches Format erhalten Sie z. B. dadurch, dass Sie jeder Note zwei Bytes zuordnen: Notenwert und Länge. Der Notenwert wird einfach anhand der folgenden schon bekannten Formel berechnet:
F=27,5*2I/12
F ist die Frequenz des Tons, und I ist die Anzahl der Halbtonschritte, die Sie ab dem Ton A0 gehen müssen, um einen bestimmten Ton zu erreichen. Ein Notenwert von 1 ist also der Ton A#0. A#0 wird nun als der tiefste Ton definiert, den Ihr Player abspielen kann, der Notenwert 0 wird für Pausen reserviert. Dadurch fällt der Ton A0 weg, aber dies ist nicht so schlimm – der Ton A0 ist so tief, dass er in kaum einem Stück vorkommt.
Die Notenlänge ist nun stets ein Vielfaches der kürzesten Note, die ein Stück enthält. Wenn also die Zweiunddreißigstel-Note die kürzeste Note ist, die Ihr Stück enthält (kürzere Noten enthält kaum ein Stück), dann beschreibt eine Notenlänge von 4 eine Achtel-Note. Bei einem langsamen Walzer dagegen, in dem der Grundtakt der ¾-Takt ist, kommen oft nur Achtel-Noten als kürzeste Einheit vor. Auf diese Weise erhält dann die Viertel-Note den Längenwert 2.
Nun gibt es nicht nur die Standard-Tonleitern und den Grundton A0, sondern jede Instrumenten-Art verwendet einen anderen tiefsten Grundton. Klavierpartituren verwenden z. B. oft den Grundton C1 als tiefsten Ton, und auch viele alte Notensatzprogramme (vor Allem auf dem C64) verwenden oft C1 als tiefsten Ton. Dagegen werden z. B. Bass-Instrumente so notiert, dass die Notenwerte 15 Halbnoten tiefer liegen (Bassschlüssel), und somit sogar Frequenzen unter 10 Hz noch abgebildet werden können. Moderne PC-Programme, die die MIDI-Notation verwenden, definieren dagegen A0 als tiefsten Ton, allerdings bekommt A0 hier den Notenwert 0 zugeordnet.
Ich habe nun das nächste Listing, das "die blaue Donau" mit drei Flöten-Stimmen abspielt, so erstellt, dass der tiefste Ton C1 ist – allerdings wird dieser Ton nicht benutzt, weil hier wieder die Pause den Notenwert 0 bekommt. Die Sample-Rate habe ich so eingestellt, dass 11.025 Bytes pro Sekunde dem Ton C4 entspricht, und nicht dem Ton C1.
Auf diese Weise erhalten Sie die beste Klangqualität auf dem Arduino. Kommen wir nun zu den Notenwerten.
Der Ton C1 hat hier den tiefst möglichen Wert (0), der aber für Pausen benutzt wird. Ab C1 zählen Sie wieder Halbtonschritte, ein Notenwert von 1 entspricht also dem Ton C#1, ein Notenwert von 2 dem Ton D1. Einem Notenwert-Byte folgt stets ein Längenbyte, das stets das Vielfache einer Zweiunddreißigstel-Note ist. Der Grund dafür, meine Notenwerte so zu wählen, wie sie sind, ist, dass ich meinen Song vom Game Maker für den C64 übernommen habe.
Nun müssen Sie nur noch den Song selbst notieren. Normalerweise würden Sie dafür die Notenschrift benutzen, aber diese ist für ein Computerprogramm ungeeignet, da Sie hier die einzelnen Stimmen nicht untereinanderschreiben können. Deshalb stehen die Stimmen in einem Songarray mit dem Namen Song hintereinander. Damit der Player den Song korrekt starten kann, fangen die Daten für eine Stimme stets mit einem Byte an, bei dem das oberste Bit gesetzt ist (0x8X). Dies bedeutet in etwa "Achtung, jetzt kommt eine neue Stimme", wobei die untersten 7 Bits die entsprechende Nummer der Stimme angeben. Der Song im nächsten Beispiel benutzt 3 Stimmen und fängt bei Stimme 0 an. Dem Byte für die Stimme folgen nun 6 Bytes:
- Byte 0: Anschwellen (attack) (1=langsam, 255=schnell)
- Byte 1: Lautstärke, bis zu der der Ton anschwellen soll (255=volle Lautstärke)
- Byte 2: Ausklingen (release) (1=langsam, 255=schnell)
- Byte 3: Lo-Byte des Zeigers auf den Beginn der Stimme im Array Instr (hier stehen die Instrumenten-Samples)
- Byte 4: Hi-Byte des Zeigers auf den Beginn der Stimme im Array Instr (hier stehen die Instrumenten-Samples)
- Byte 5: Lo-Byte der Sample-Länge für das Instrument
- Byte 6: Hi-Byte der Sample-Länge für das Instrument
- Byte 7: Sound-Flag (hier wird nur das oberste Bit Nr. 7 benutzt)
Der Player spielt nun einen Intrumenten-Sound immer dann in einer Schleife ab, wenn Bit 7 im Sound-Flag 1 ist, ansonsten wird für den Sound keine Schleife benutzt. Auf diese Weise können Sie sehr kurze Intrumenten-Samples benutzen, z. B. bei Flöten-Tönen, die fast einer Sinuswelle entsprechen. Aber auch bei Gitarrenklängen müssen Sie quasi nur eine einzige Schwingung einer Saite abtasten. Allerdings treten bei zu schnellen Wiederholungen von Wellenformen mit weniger als 512 Bytes unter Umständen Knackser auf. Zusätzlich zum Instrument benötigen Sie natürlich noch die Song-Daten. Dieser wird in der folgenden Form in einem Array abgelegt:
byte Song[]=
{
… Hier stehen die Daten für die Noten …
};
Sie fragen sich vielleicht, wie Sie Zahlen für die Notenwerte erstellen sollen, wenn Sie einen eigenen Song erstellen wollen. Zugegebenermaßen ist dies nicht trivial, vor allem, wenn Sie nur Noten in der Form besitzen, wie Sie diese z. B. in Musikgeschäften erwerben können. Im Endeffekt müssen Sie in diesem Fall die einzelnen Noten aufwendig per Hand in Zahlen umwandeln. Eine andere Möglichkeit ist, im Internet nach freien MIDI-Dateien zu suchen, deren Inhalt Sie dann mit freien MIDI-Readern anzeigen lassen können. Allerdings verwenden MIDI-Dateien mehr Flags, als mein einfaches Song-Format.
Natürlich kann ich mich in einem einzigen Artikel nicht auch noch ausgiebig über das MIDI-Format auslassen. Für einen einfach gestrickten Player fehlt allerdings nicht mehr viel, außer der Steuerung für die Stimmen selbst. Diese müssen unabhängig voneinander arbeiten, aber trotzdem die einzelnen Noten synchron zu einem bestimmten Takt abspielen. Am einfachsten ist es hier, einen strukturierten Datentyp der folgenden Form zu benutzen:
struct voice
{
unsigned long int CurrentTime; // entspricht micros()
unsigned long int SongStart; // Zeiger auf die erste Note
unsigned long int SongPos; // Zeiger auf die Aktuelle Note
unsigned long int NoteStartTime; // Zeitzähler Notenstart
unsigned long int NoteEndTime; // Zeitzähler Notenende
unsigned long int SampleStartTime; // Der Wert von CurrentTime, bei dem der letzte Instrumenten-Sample neu gestartet wurde
unsigned long int SampleStart; // Zeiger auf den Sample-Start des Instrumentes
unsigned long int SamplePos;
unsigned long int SampleLen;
bool LoopFlag; // Ist true, wenn der Instrumenten-Sample ineiner Schleife abgespielt werden soll
int ES; // aktueller Zustand der Hüllkurven-Generators
double SampleFact; // Dieser Faktor wählt die Frequenz der aktuellen Note aus
double Vol; // Aktuelle Lautstärke für den aktuellen Ton
double Attack; // Attack-Wert: Dieser Wert wird jede Mikrosekunde zu Vol addiert, wenn ES=1 ist
double Release; // Release-Wert: Dieser Wert wird jede Mikrosekunde zu Vol addiert, wenn ES=2 ist
};
Der Attack-Wert und der Release-Wert werden hier einfachheitshalber so definiert: Ein Wert von 0,000001 bedeutet, dass zu Vol jede Mikrosekunde der Wert 0,000001 addiert wird. Dies bedeutet, z. B. für Attack=1, dass die volle Lautstärke in einer Sekunde erreicht ist. Ein Wert von 0,00001 bedeutet also, dass zu Vol jede Mikrosekunde ein Wert von 0,00001 addiert wird, und die volle Lautstärke in 1/10 Sekunde erreicht ist. Beim Release-Wert ist dies ähnlich, nur dass von Vol jede Mikrosekunde der Wert für Release subtrahiert wird. Sie haben nun sämtliche Informationen zusammen, um das Listing Donau.ino zu verstehen.
// Donau.ino
Hinweis: Dieses Listing habe ich auch auf meine Homepage gestellt [2]. Den Download finden Sie im Bereich "Mikrocontroller programmieren mit C"
byte Song[]=
{
// Stimme 0 Längen: 2=1/16, 4=1/8, 8=1/4, 16=1/2, 32=1/1
0x80,0,8,43,4,42,4,43,8,0,8,45,4,44,4,45,8,0,8,47,4,45,4,47,8,48,8,
50,8,52,8,0,8,50,4,49,4,50,8,0,8,52,4,51,4,52,8,0,8,52,4,51,4,52,8,
53,8,55,8,57,8,0,8,53,4,55,4,57,4,43,4,53,32,53,4,55,4,57,4,43,4,52,
32,52,4,53,4,55,4,43,4,50,4,0,4,53,4,0,4,43,4,0,4,48,4,0,4,53,4,0,4,
43,4,0,4,47,4,0,4,53,4,0,4,43,4,0,4,45,4,0,4,50,4,0,4,41,4,0,4,43,4,
0,4,48,4,0,4,40,4,0,4,41,4,0,4,47,4,0,4,38,4,0,4,0,16,0,8,0,16,0,8,
0,16,0,8,36,8,40,8,43,8,43,16,55,8,55,8,0,8,52,8,52,8,0,8,36,8,36,8,
40,8,43,8,43,16,55,8,55,8,0,8,53,8,53,8,0,8,35,8,35,8,38,8,45,8,45,
16,57,8,57,8,0,8,53,8,53,8,0,8,35,8,35,8,38,8,45,8,45,16,57,8,57,8,
0,8,52,8,52,8,0,8,36,8,36,8,40,8,43,8,48,16,60,8,60,8,0,8,55,8,55,8,
0,8,36,8,36,8,40,8,43,8,48,16,60,8,60,8,0,8,57,8,57,8,0,8,38,8,38,8,
41,8,45,4,0,4,45,28,0,4,42,8,43,8,52,28,0,4,48,8,40,8,40,16,38,8,45,
16,43,8,48,8,0,4,48,4,48,8,0,8,60,4,0,4,59,4,0,4,59,4,0,4,57,4,0,4,
57,4,0,4,0,8,57,4,0,4,56,4,0,4,56,4,0,4,57,4,0,4,57,4,0,4,0,8,50,4,
0,4,50,4,0,4,52,16,50,8,0,8,50,4,0,4,50,4,0,4,57,16,55,8,0,8,60,4,0,
4,59,4,0,4,59,4,0,4,57,4,0,4,57,4,0,4,0,8,57,4,0,4,59,4,0,4,62,4,0,
4,60,4,0,4,60,4,0,4,0,8,54,4,0,4,57,4,0,4,57,16,55,8,54,12,52,4,48,
4,45,4,52,4,52,4,52,8,50,8,43,8,60,4,0,4,59,4,0,4,59,4,0,4,57,4,0,4,
57,4,0,4,0,8,57,4,0,4,56,4,0,4,56,4,0,4,57,4,0,4,57,4,0,4,0,8,50,4,
0,4,50,4,0,4,52,16,50,8,0,8,50,4,0,4,50,4,0,4,57,16,55,8,0,8,60,
4,0,4,59,4,0,4,59,4,0,4,57,4,0,4,57,4,0,4,0,8,57,4,0,4,59,4,0,4,62,
4,0,4,60,4,0,4,60,4,0,4,0,8,54,4,0,4,57,4,0,4,57,16,55,8,54,12,52,4,
48,4,45,4,52,4,52,4,52,8,50,8,43,8,0,16,55,2,54,2,55,2,54,2,55,2,54,
2,55,2,54,2,55,2,54,2,55,2,54,2,55,2,54,2,55,2,54,2,55,2,54,2,55,2,
54,2,55,2,54,2,55,2,54,2,55,8,0,16,36,8,40,8,43,8,43,16,55,8,55,8,0,
8,52,8,52,8,0,8,36,8,36,8,40,8,43,8,43,16,55,8,55,8,0,8,53,8,53,8,0,
8,35,8,35,8,38,8,45,8,45,16,57,8,57,8,0,8,53,8,53,8,0,8,35,8,35,8,
38,8,45,8,45,16,57,8,57,8,0,8,52,8,52,8,0,8,36,8,36,8,40,8,43,8,48,
16,60,8,60,8,0,8,55,8,55,8,0,8,36,8,36,8,40,8,43,8,48,16,60,8,60,8,
0,8,57,8,57,8,0,8,38,8,38,8,41,8,45,4,0,4,45,28,0,4,42,8,43,8,52,28,
0,4,48,8,40,8,40,16,38,8,45,16,43,8,48,8,0,4,48,4,48,8,0,8,60,4,0,4,
59,4,0,4,59,4,0,4,57,4,0,4,57,4,0,4,0,8,57,4,0,4,56,4,0,4,56,4,0,4,
57,4,0,4,57,4,0,4,0,8,50,4,0,4,50,4,0,4,52,16,50,8,0,8,50,4,0,4,50,
4,0,4,57,16,55,8,0,8,60,4,0,4,59,4,0,4,59,4,0,4,57,4,0,4,57,4,0,4,0,
8,57,4,0,4,59,4,0,4,62,4,0,4,60,4,0,4,60,4,0,4,0,8,54,4,0,4,57,4,0,
4,57,16,55,8,54,12,52,4,48,4,45,4,52,4,52,4,52,8,50,8,43,8,60,4,0,4,
59,4,0,4,59,4,0,4,57,4,0,4,57,4,0,4,0,8,57,4,0,4,56,4,0,4,56,
4,0,4,57,4,0,4,57,4,0,4,0,8,50,4,0,4,50,4,0,4,52,16,50,8,0,8,50,4,
0,4,50,4,0,4,57,16,55,8,0,8,60,4,0,4,59,4,0,4,59,4,0,4,57,4,0,4,57,4,
0,4,0,8,57,4,0,4,59,4,0,4,62,4,0,4,60,4,0,4,60,4,0,4,0,8,54,4,0,4,
57,4,0,4,57,16,55,8,54,12,52,4,48,4,45,4,52,4,52,4,52,8,50,8,43,
8,0,32,255,255,
// Stimme 1 Längen: 2=1/16, 4=1/8, 8=1/4, 16=1/2, 32=1/1
0x81,31,8,31,8,31,8,0,16,0,8,0,16,0,8,0,16,0,8,0,16,0,8,0,16,0,8,0,
16,0,8,31,8,0,8,0,8,0,16,0,8,0,8,35,8,35,8,31,8,0,8,0,8,0,8,36,8,36,
8,31,8,0,16,31,24,31,24,31,24,31,24,31,24,31,24,31,8,0,16,29,8,0,16,
26,8,0,16,0,16,0,8,24,8,40,8,40,8,31,8,40,8,40,8,24,8,40,8,40,8,31,
8,40,8,40,8,26,8,41,8,41,8,31,8,41,8,41,8,26,8,41,8,41,8,31,8,41,8,
41,8,19,8,41,8,41,8,23,8,41,8,41,8,26,8,41,8,41,8,29,8,38,8,35,8,24,
8,40,8,40,8,31,8,40,8,40,8,24,8,40,8,40,8,31,8,40,8,40,8,24,8,40,8,
40,8,28,8,40,8,40,8,31,8,40,8,40,8,14,8,19,8,19,8,17,8,41,8,41,8,
21,8,41,8,41,8,26,8,36,8,38,8,26,8,29,8,33,4,0,4,19,8,41,8,41,8,23,
8,38,8,38,8,24,8,40,8,40,8,31,8,40,8,40,8,17,8,26,16,19,8,29,16,24,
8,0,8,24,8,0,16,0,8,26,8,36,8,36,8,30,8,36,8,36,8,26,8,36,8,36,8,
30,8,36,8,36,8,19,8,35,8,35,8,26,8,35,8,35,8,19,8,35,8,35,8,26,8,35,
8,35,8,26,8,36,8,36,8,30,8,36,8,36,8,26,8,36,8,36,8,26,8,36,8,27,8,
28,8,43,16,24,8,40,16,36,8,33,8,30,8,19,8,0,16,26,8,36,8,36,8,30,8,
36,8,36,8,26,8,36,8,36,8,30,8,36,8,36,8,19,8,35,8,35,8,26,8,35,8,
35,8,19,8,35,8,35,8,26,8,35,8,35,8,26,8,36,8,36,8,30,8,36,8,36,8,
26,8,36,8,36,8,26,8,36,8,27,8,28,8,43,16,24,8,40,16,36,8,33,8,30,8,
19,8,0,16,38,24,31,24,38,8,0,16,0,16,0,8,24,8,40,8,40,8,31,8,40,8,
40,8,24,8,40,8,40,8,31,8,40,8,40,8,26,8,41,8,41,8,31,8,41,8,41,8,26,
8,41,8,41,8,31,8,41,8,41,8,19,8,41,8,41,8,23,8,41,8,41,8,26,8,41,8,
41,8,29,8,38,8,35,8,24,8,40,8,40,8,31,8,40,8,40,8,24,8,40,8,40,8,
31,8,40,8,40,8,24,8,40,8,40,8,28,8,40,8,40,8,31,8,40,8,40,8,14,8,
19,8,19,8,17,8,41,8,41,8,21,8,41,8,41,8,26,8,36,8,38,8,26,8,29,8,
33,4,0,4,19,8,41,8,41,8,23,8,38,8,38,8,24,8,40,8,40,8,31,8,40,8,40,
8,17,8,26,16,19,8,29,16,24,8,0,8,24,8,0,16,0,8,26,8,36,8,36,8,30,8,
36,8,36,8,26,8,36,8,36,8,30,8,36,8,36,8,19,8,35,8,35,8,26,8,35,8,
35,8,19,8,35,8,35,8,26,8,35,8,35,8,26,8,36,8,36,8,30,8,36,8,36,8,
26,8,36,8,36,8,26,8,36,8,27,8,28,8,43,16,24,8,40,16,36,8,33,8,30,8,
19,8,0,16,26,8,36,8,36,8,30,8,36,8,36,8,26,8,36,8,36,8,30,8,36,8,36,
8,19,8,35,8,35,8,26,8,35,8,35,8,19,8,35,8,35,8,26,8,35,8,35,8,26,8,
36,8,36,8,30,8,36,8,36,8,26,8,36,8,36,8,26,8,36,8,27,8,28,8,43,16,
24,8,40,16,36,8,33,8,30,8,19,8,0,32,255,255,
// Stimme 2 Längen: 2=1/16, 4=1/8, 8=1/4, 16=1/2, 32=1/1
0x82,35,4,38,4,35,4,38,4,35,4,38,4,36,4,40,4,36,4,40,4,36,4,40,4,
38,4,41,4,38,4,41,4,38,4,41,4,40,4,43,4,40,4,43,4,36,4,40,4,41,4,
43,4,41,4,43,4,35,4,41,4,40,4,43,4,40,4,43,4,36,4,40,4,40,4,43,4,40,
4,43,4,36,4,40,4,38,8,0,16,0,16,0,8,0,8,41,8,41,8,35,8,0,16,0,8,40,
8,40,8,36,8,0,16,38,8,41,16,36,8,41,16,35,8,41,16,41,24,40,24,38,24,
0,16,0,8,0,16,0,8,0,16,0,8,0,16,0,8,0,8,36,8,36,8,0,8,36,8,36,8,0,8,
36,8,36,8,0,8,36,8,36,8,0,8,38,8,38,8,0,8,38,8,38,8,0,8,38,8,38,8,
0,8,38,8,38,8,0,8,38,8,38,8,0,8,38,8,38,8,0,8,38,8,38,8,0,8,38,8,
38,8,0,8,36,8,36,8,0,8,36,8,36,8,0,8,36,8,36,8,0,8,36,8,36,8,0,8,
36,8,36,8,0,8,36,8,36,8,0,8,36,8,36,8,0,8,36,8,36,8,0,8,33,8,33,8,
0,8,36,8,36,8,0,16,0,8,0,16,0,8,0,8,35,8,35,8,0,8,35,8,35,8,0,8,36,
8,36,8,0,8,36,8,36,8,36,16,35,8,41,16,38,8,40,8,0,4,40,4,40,8,0,8,
52,4,0,4,50,4,0,4,50,4,0,4,48,4,0,4,48,4,0,4,0,8,48,4,0,4,47,4,0,4,
47,4,0,4,48,4,0,4,48,4,0,4,0,8,42,4,0,4,45,4,0,4,0,8,38,8,38,8,0,8,
38,8,38,8,0,8,38,8,38,8,0,8,52,4,0,4,50,4,0,4,50,4,0,4,48,4,0,4,48,
4,0,4,0,8,48,4,0,4,56,4,0,4,47,4,0,4,57,4,0,4,57,4,0,4,0,8,45,4,0,
4,48,4,0,4,47,16,47,8,45,16,43,8,42,4,42,4,42,8,45,8,35,8,0,16,50,4,
0,4,48,4,0,4,48,4,0,4,0,8,48,4,0,4,47,4,0,4,47,4,0,4,48,4,0,4,48,4,
0,4,0,8,42,4,0,4,45,4,0,4,0,8,38,8,38,8,0,8,38,8,38,8,0,8,38,8,38,
8,0,8,52,4,0,4,50,4,0,4,50,4,0,4,48,4,0,4,48,4,0,4,0,8,48,4,0,4,56,
4,0,4,47,4,0,4,57,4,0,4,57,4,0,4,0,8,45,4,0,4,48,4,0,4,47,16,47,8,
45,16,43,8,42,4,42,4,42,8,45,8,35,8,0,16,41,24,38,24,41,8,0,16,0,
16,0,8,0,8,36,8,36,8,0,8,36,8,36,8,0,8,36,8,36,8,0,8,36,8,36,8,0,8,
38,8,38,8,0,8,38,8,38,8,0,8,38,8,38,8,0,8,38,8,38,8,0,8,38,8,38,8,
0,8,38,8,38,8,0,8,38,8,38,8,0,8,38,8,38,8,0,8,36,8,36,8,0,8,36,8,
36,8,0,8,36,8,36,8,0,8,36,8,36,8,0,8,36,8,36,8,0,8,36,8,36,8,0,8,
36,8,36,8,0,8,36,8,36,8,0,8,33,8,33,8,0,8,36,8,36,8,0,16,0,8,0,16,
0,8,0,8,35,8,35,8,0,8,35,8,35,8,0,8,36,8,36,8,0,8,36,8,36,8,36,16,
35,8,41,16,38,8,40,8,0,4,40,4,40,8,0,8,52,4,0,4,50,4,0,4,50,4,0,4,
48,4,0,4,48,4,0,4,0,8,48,4,0,4,47,4,0,4,47,4,0,4,48,4,0,4,48,4,0,4,
0,8,42,4,0,4,45,4,0,4,0,8,38,8,38,8,0,8,38,8,38,8,0,8,38,8,
38,8,0,8,52,4,0,4,50,4,0,4,50,4,0,4,48,4,0,4,48,4,0,4,0,8,48,4,0,4,
56,4,0,4,47,4,0,4,57,4,0,4,57,4,0,4,0,8,45,4,0,4,48,4,0,4,47,16,47,
8,45,16,43,8,42,4,42,4,42,8,45,8,35,8,0,16,50,4,0,4,48,4,0,4,48,4,
0,4,0,8,48,4,0,4,47,4,0,4,47,4,0,4,48,4,0,4,48,4,0,4,0,8,42,4,0,4,
45,4,0,4,0,8,38,8,38,8,0,8,38,8,38,8,0,8,38,8,38,8,0,8,52,4,0,4,50,
4,0,4,50,4,0,4,48,4,0,4,48,4,0,4,0,8,48,4,0,4,56,4,0,4,47,4,0,4,57,
4,0,4,57,4,0,4,0,8,45,4,0,4,48,4,0,4,47,16,47,8,45,16,43,8,42,4,42,
4,42,8,45,8,35,8,0,32,255,255
};
signed char Inst[]=
{
0x0e,0x3a,0x5f,0x77,0x7d,0x70,0x53,0x24,0xf2,0xc1,0xae,0xb7,0xd6,
0xfd,0x13,0x18,0x07,0xea,0xca,0xaf,0x9e,0x9d,0xb1,0xce,0xf4,0x1c,
0x46,0x65,0x78,0x78,0x64,0x3e,0x11,0xe1,0xba,0xad,0xc1,0xe6,0x06,
0x17,0x11,0xfc,0xe0,0xbd,0xa3,0x96,0x9e,0xb8,0xdd,0x03,0x2e,0x54,
0x71,0x7e,0x73,0x5b,0x33,0x01,0xd3,0xb0,0xb2,0xcb,0xef,0x0d,0x1a,
0x11,0xf4,0xd4,0xb3,0x9b,0x94,0xa1,0xbf,0xe9,0x14,0x40,0x62,0x76,
0x7e,0x72,0x4f,0x1f,0xf3,0xc7,0xae,0xb7,0xd5,0xfa,0x15,0x1a,0x04,
0xe7,0xc3,0xa8,0x95,0x92,0xaa,0xcc,0xf8,0x22,0x4a,0x69,0x7c,0x7a,
0x63,0x41,0x10,0xe3,0xb8,0xab,0xc0,0xe4,0x01,0x16,0x0f,0xf9,0xd8,
0xb6,0x9c,0x92,0x9b,0xb6,0xda,0x03,0x30,0x57,0x70,0x7d,0x76,0x59,
0x2f,0x05,0xd8,0xb4,0xb0,0xca,0xeb,0x0a,0x17,0x0b,0xf1,0xcb,0xae,
0x98,0x91,0x9e,0xc0,0xe6,0x14,0x3f,0x61,0x77,0x7d,0x6f,0x50,0x24,
0xf5,0xca,0xaf,0xb7,0xd4,0xf7,0x14,0x15,0x04,0xe6,0xc1,0xa4,0x93,
0x93,0xa8,0xca,0xf6,0x22,0x4a,0x6a,0x7a,0x79,0x67,0x43,0x18,0xe5,
0xb9,0xb0,0xc2,0xe1,0x02,0x16,0x13,0xfc,0xd8,0xb6,0x99,0x8e,0x95,
0xb1,0xd8,0x05,0x31,0x58,0x71,0x7c,0x75,0x5c,0x35,0x06,0xd5,0xb5,
0xb1,0xcb,0xf0,0x0d,0x19,0x0d,0xf0,0xcf,0xad,0x96,0x8e,0x9e,0xbe,
0xe8,0x14,0x41,0x63,0x7a,0x7d,0x6f,0x51,0x25,0xf8,0xca,0xb1,0xba,
0xda,0xfe,0x16,0x1a,0x0a,0xe4,0xc1,0xa3,0x91,0x92,0xa8,0xcb,0xf6,
0x26,0x51,0x6c,0x7c,0x79,0x68,0x43,0x15,0xe8,0xc1,0xb8,0xc6,0xe9,
0x08,0x1b,0x14,0xfd,0xd9,0xb6,0x9a,0x90,0x96,0xb3,0xd9
};
unsigned long int BeatLen;
double NoteTab[256];
struct voice
{
unsigned long int CurrentTime;
unsigned long int SongStart;
unsigned long int SongPos;
unsigned long int NoteStartTime;
unsigned long int NoteEndTime;
unsigned long int SampleStartTime;
unsigned long int SampleStart;
unsigned long int SamplePos;
unsigned long int SampleLen;
bool LoopFlag;
int ES;
double SampleFact;
double Vol;
double Attack;
double Release;
};
voice Voices[3]; // 3 Stimmen Mono (an DAC-Pin 0)
unsigned long int CPrev; // Hilfsvariable für den zuletzt eingelesenen Wert von micros()
// Alle Stimmen müssen, um synchron zu laufen, stets den selben Wert für die interne Uhr benutzen
void setup()
{
InitNoteTab(689.0625); // Sample-Rate von C0 ist 689,0625 Hz
StartSong(70,16); // Tempo:70 Schläge pro Sekunde, 1/16 Note ist die kleinst mögliche Einheit
}
void InitNoteTab(double G)
{
int i;
double F;
G=G/1000000.0;
analogWriteResolution(10);
for (int i=0; i<128; i++)
{
F=pow(2,((double)(i)/12.0)); // 2*i^(1/12)
NoteTab[i]=G*F;
}
NoteTab[0]=0.0; CPrev=micros();
StartSong(70,16);
}
void StartSong(int Speed, int RefNote) // Setzen der Parameter für die 3 Stimmen (statisch)
{
Voices[0].SampleFact=0.0; Voices[0].SampleStart=0; Voices[0].SampleLen=271;
Voices[0].SampleStartTime=CPrev;
Voices[0].ES=0; Voices[0].Vol=0.0; Voices[0].Attack=0.00002;
Voices[0].Release=0.00000025; // langes Ausklingen
Voices[0].NoteStartTime=CPrev; Voices[0].NoteEndTime=CPrev;
GetVoiceStart(0,Song,Voices[0].SongPos); Voices[0].SongStart=Voices[0].SongPos; // Songstart Stimme 0
Voices[1].SampleFact=0.0; Voices[1].SampleStart=0; Voices[1].SampleLen=271; Voices[1].SampleStartTime=CPrev;
Voices[1].ES=0; Voices[1].Vol=0.0; Voices[1].Attack=0.00002; Voices[1].Release=0.000001; // kurzes Ausklingen
Voices[1].NoteStartTime=CPrev; Voices[1].NoteEndTime=CPrev;
GetVoiceStart(1,Song,Voices[1].SongPos); Voices[1].SongStart=Voices[1].SongPos; // Songstart Stimme 1
Voices[2].SampleFact=0.0; Voices[2].SampleStart=0; Voices[2].SampleLen=271; Voices[2].SampleStartTime=CPrev;
Voices[2].ES=0; Voices[2].Vol=0.0; Voices[2].Attack=0.00002; Voices[2].Release=0.000001; // kurzes Ausklingen
Voices[2].NoteStartTime=CPrev; Voices[2].NoteEndTime=CPrev;
GetVoiceStart(2,Song,Voices[2].SongPos); Voices[2].SongStart=Voices[2].SongPos; // Songstart Stimme 2
BeatLen=60000000/Speed/RefNote; // Hier:RefNote=1/16, Speed=70
}
void GetVoiceStart(int Voice, byte *SongArray, unsigned long int &StartPos)
{
StartPos=0;
while (SongArray[StartPos]!=(Voice|0x80)) { StartPos++; }
StartPos++; // Song-Zeiger direkt hinter das Stimmen-Token setzen
}
double SoundLoop(int i)
{
double OutD; // Output für Mixer
double Temp; // temporäre Variable
int Out; // Output für DAC
double TimeD=(double)(Voices[i].CurrentTime-Voices[i].SampleStartTime); // Sample Timer holen
TimeD*=Voices[i].SampleFact; // Sample-Frequenz einstellen
Voices[i].SamplePos=Voices[i].SampleStart+((unsigned long int)(TimeD)%Voices[i].SampleLen); // Flötenton in einer Schleife abspielen
OutD=(double)(Inst[Voices[i].SamplePos]); // aktuellen Instrumenten-Sample auslesen
Temp=(double)(Voices[i].CurrentTime-CPrev); // Temp=Seit dem letzten Aufruf vergangene Mikrosekunden
if (Voices[i].ES==1) // Attack-State=1
{
Temp*=Voices[i].Attack; // Attack-Wert holen
Voices[i].Vol+=Temp; // Attack-Wert zu Vol addieren
if (Voices[i].Vol>1.0) {
Voices[i].Vol=1.0; Voices[i].ES=2; }
}
else if (Voices[i].ES==2) // Realease-State=2
{
Temp*=Voices[i].Release; // Release-Wert holen
Voices[i].Vol-=Temp; // Release-Wert von Vol subtrahieren
if (Voices[i].Vol<0.0) // Ton ist ausgeklungen?
{
Voices[i].Vol=0.0; // Ton wird beendet
Voices[i].ES=0; // Ton muss von außen erneut angestoßen werden
}
}
OutD=OutD*Voices[i].Vol; // OutD mit aktueller Lautstärke der Hüllkurve skalieren
return OutD; // OutD wird skaliert zurückgegeben
}
}
void loop()
{
unsigned long int NoteLen;
int i=0;
double OutD=0.0;
unsigned long int TI=micros(); // alle drei Stimmen mit der aktuellen Uhr synchronisieren
for (i=0; i<3; i++)
{
Voices[i].CurrentTime=TI; // alle drei Stimmen mit der aktuellen Uhr synchronisieren
if (Voices[i].CurrentTime>Voices[i].NoteEndTime) //Zeitzähler für aktuelle Note abgelaufen?
{
Voices[i].SongPos+=2; // Nächste Note holen
if (Song[Voices[i].SongPos]==0xff) { Voices[i].SongPos=Voices[i].SongStart; } // Repeat-Token? Dann zum Start zurück
NoteLen=BeatLen*Song[Voices[i].SongPos+1]; //Notenlänge holen
Voices[i].NoteStartTime=Voices[i].CurrentTime; Voices[i].NoteEndTime=Voices[i].CurrentTime+NoteLen; // Zeitzähler aktualisieren
if (Song[Voices[i].SongPos]!=0) // Keine Pause? Dann Samplerate aktualisieren
{
Voices[i].SampleFact=NoteTab[Song[Voices[i].SongPos]];
Voices[i].ES=1; Voices[i].Vol=1.0;
Voices[i].SampleStartTime=Voices[i].
CurrentTime; // Sample start aktualisieren
}
}
OutD+=SoundLoop(i); // Mixer bildet die Summe aller drei Stimmen als double
}
CPrev=TI; // Envelope Generator neu kalibirieren
analogWrite(DAC1,(int)(OutD)+512);
}
Wenn Sie alles richtig gemacht haben, dann spielt Ihr Arduino nun den Walzer "An der blauen Donau" in einer Endlosschleife immer wieder ab. Dies geht so lange gut, bis irgendwann der Zähler für micros() überläuft und danach nur noch Datenmüll abgespielt wird, der einem hohen weißen Rauschen entspricht. Bis dies geschieht, verstreichen 4.294.967.295 Mikrosekunden. Dies entspricht in etwa 71 Stunden.