MIDI zu polyphoner Rechteckwelle auf Arduino

Ich habe jetzt seit ein paar Tagen damit zu kämpfen, und ich würde wirklich gerne mit meinem Projekt weitermachen. Das hält mich leider auf. Ich versuche, MIDI-Signale abzurufen, die über USB an das Arduino (Uno) gesendet werden, sie zu dekodieren und dann eine nicht native Tonbibliothek ( https://code.google.com/p/rogue-code/wiki /ToneLibraryDocumentation), um mehrere Rechteckwellentöne gleichzeitig zu spielen. Ich habe noch 6 E / A-Ports auf meinem Arduino, mit denen ich arbeiten kann, also erwarte ich, dass mich das auf 6 Töne beschränkt. Ich habe verschiedene Methoden ausprobiert, um dies zum Laufen zu bringen, konnte aber keine davon beenden, weil ich auf die eine oder andere Weise auf eine Straßensperre gestoßen bin. Meine Hoffnung ist es, mehrere Töne zu spielen, unabhängig davon, ob sie vom selben Kanal (Akkorde) oder von verschiedenen Kanälen (getrennte Instrumente) stammen, aber ich finde immer noch keinen guten Weg, dies zu tun.

Das Einlesen der MIDI-Daten ist nicht der schwierige Teil. Ich habe den folgenden Code, der von Greg Kennedy geschrieben und in den Arduino-Foren ( http://forum.arduino.cc/index.php?topic=79326.0 ) veröffentlicht wurde , um Daten von einem bestimmten MIDI-Kanal abzurufen und mit dem nativen abzuspielen Tone () Funktion, aber leider kann es nur eine Note auf einmal spielen.

// A very simple MIDI synth.
// Greg Kennedy 2011

#include <avr/pgmspace.h>

#define statusLed 13
#define tonePin 7

// MIDI channel to answer to, 0x00 - 0x0F
#define myChannel 0x00
// set to TRUE and the device will respond to all channels
#define respondAllChannels false

// midi commands
#define MIDI_CMD_NOTE_OFF 0x80
#define MIDI_CMD_NOTE_ON 0x90
#define MIDI_CMD_KEY_PRESSURE 0xA0
#define MIDI_CMD_CONTROLLER_CHANGE 0xB0
#define MIDI_CMD_PROGRAM_CHANGE 0xC0
#define MIDI_CMD_CHANNEL_PRESSURE 0xD0
#define MIDI_CMD_PITCH_BEND 0xE0

// this is a placeholder: there are
//  in fact real midi commands from F0-FF which
//  are not channel specific.
// this simple synth will just ignore those though.
#define MIDI_CMD_SYSEX 0xF0

// a dummy "ignore" state for commands which
//  we wish to ignore.
#define MIDI_IGNORE 0x00

// midi "state" - which data byte we are receiving
#define MIDI_STATE_BYTE1 0x00
#define MIDI_STATE_BYTE2 0x01

// MIDI note to frequency
//  This isn't exact and may sound a bit detuned at lower notes, because
//  the floating point values have been rounded to uint16.
//  Based on A440 tuning.

// I would prefer to use the typedef for this (prog_uint16_t), but alas that triggers a gcc bug
// and does not put anything into the flash memory.

// Also note the limitations of tone() which at 16mhz specifies a minimum frequency of 31hz - in other words, notes below
// B0 will play at the wrong frequency since the timer can't run that slowly!
uint16_t frequency[128] PROGMEM = {8, 9, 9, 10, 10, 11, 12, 12, 13, 14, 15, 15, 16, 17, 18, 19, 21, 22, 23, 24, 26, 28, 29, 31, 33, 35, 37, 39, 41, 44, 46, 49, 52, 55, 58, 62, 65, 69, 73, 78, 82, 87, 92, 98, 104, 110, 117, 123, 131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 233, 247, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951, 4186, 4435, 4699, 4978, 5274, 5588, 5920, 5920, 6645, 7040, 7459, 7902, 8372, 8870, 9397, 9956, 10548, 11175, 11840, 12544};

//setup: declaring iputs and outputs and begin serial
void setup() {
  pinMode(statusLed,OUTPUT);   // declare the LED's pin as output

  pinMode(tonePin,OUTPUT);           // setup tone output pin

  //start serial with midi baudrate 31250
  // or 38400 for debugging (eg MIDI over serial from PC)
  Serial.begin(31250);

  // indicate we are ready to receive data!
  digitalWrite(statusLed,HIGH);
}

//loop: wait for serial data
void loop () {
  static byte note;
  static byte lastCommand = MIDI_IGNORE;
  static byte state;
  static byte lastByte;

  while (Serial.available()) {

    // read the incoming byte:
    byte incomingByte = Serial.read();

    // Command byte?
    if (incomingByte & 0b10000000) {
      if (respondAllChannels ||
             (incomingByte & 0x0F) == myChannel) { // See if this is our channel
        lastCommand = incomingByte & 0xF0;
      } else { // Not our channel.  Ignore command.
        lastCommand = MIDI_IGNORE;
      }
      state = MIDI_STATE_BYTE1; // Reset our state to byte1.
    } else if (state == MIDI_STATE_BYTE1) { // process first data byte
      if ( lastCommand==MIDI_CMD_NOTE_OFF )
      { // if we received a "note off", make sure that is what is currently playing
        if (note == incomingByte) noTone(tonePin);
        state = MIDI_STATE_BYTE2; // expect to receive a velocity byte
      } else if ( lastCommand == MIDI_CMD_NOTE_ON ){ // if we received a "note on", we wait for the note (databyte)
        lastByte=incomingByte;    // save the current note
        state = MIDI_STATE_BYTE2; // expect to receive a velocity byte
      }
      // implement whatever further commands you want here
    } else { // process second data byte
      if (lastCommand == MIDI_CMD_NOTE_ON) {
        if (incomingByte != 0) {
          note = lastByte;
          tone(tonePin,(unsigned int)pgm_read_word(&frequency[note]));
        } else if (note == lastByte) {
          noTone(tonePin);
        }
      }
      state = MIDI_STATE_BYTE1; // message data complete
                                 // This should be changed for SysEx
    }
  }
}

Dieser Code funktioniert hervorragend für monophones Audio, aber ich muss wirklich polyphone Töne erzeugen. Ich war schon immer eher ein PIC-Typ, also ist Arduino noch ziemlich neu für mich. Danke Leute!

BEARBEITEN: Nur um es klar zu sagen, ich möchte einen Mehrton-MIDI-Eingang in einen Mehrton-Rechteckwellenausgang umwandeln, den ich als Unterbrechersignal für einen anderen Teil des Projekts verwenden werde.

Die Wavetable-Synthese ist auf Low-End-Hardware so viel einfacher. Nicht einmal ein Scherz.
Ich kenne mich mit MIDI + Wavetable Synth mit dem Arduino nicht aus. Hätten Sie zufällig einen Link zu einer guten Datenquelle und Beispielen? Ich habe es gegoogelt, aber keine ausführliche Dokumentation darüber gefunden, nur ein paar kleine Projekte hier und da
Habe keinen gesehen. Aber es kann einfacher von Grund auf neu gemacht werden, als Rechteckwellen zusammenzumischen.
Der Ausgang muss eine Rechteckwelle sein, da er als Unterbrecher für den nächsten Teil des Projekts dient. Ich glaube, ich bin mit der Wavetable-Synthese nicht vertraut genug, um zu verstehen, wie sie auf dieses Projekt angewendet werden könnte. Danke aber für den Vorschlag, ich werde mich damit befassen.
Oh. Ich dachte, Sie würden den Arduino verwenden, um Audio zu erzeugen. Egal Dann.
Entschuldigung, es war meine Schuld, dass ich das nicht deutlich gemacht habe. Ich editiere den ursprünglichen Beitrag.
Wenn es Ihnen nichts ausmacht, können Sie erwähnen, was der nächste Teil des Projekts ist? Wenn Sie mehr Informationen haben, können wir Sie möglicherweise auf einen anderen Weg führen oder Ihnen einen besseren Vorschlag machen. Ich arbeite auch an einem Midi (Datei) zu Arduino-Projekt ... Obwohl in meinem Fall die gesamte Midi-Berechnung auf einem Computer erfolgt und über serielle Schnittstelle an das Arduino gesendet wird.
Gar nicht. Ich werde es verwenden, um das Antriebssignal auf eine Festkörper-Tesla-Spule zu modulieren. Ich habe es nicht erwähnt, weil ich nicht glaube, dass es viel helfen wird - das Signal wird einfach verwendet, um (wiederholt) einen UCC27425-Gate-Drive-Chip zu aktivieren/deaktivieren
Übrigens hätte ich erwähnen sollen, wie ich das Signal an den Arduino sende. Ich verwende die Hairless MIDI-Serial Bridge, also wird das MIDI über die USB-Verbindung an den Arduino gesendet

Antworten (2)

Polyphone Musik zu spielen, indem für jede "Stimme" separate Schaltungen verwendet werden, funktioniert sehr gut, wenn Musik logisch in eine Anzahl von Einzelstimmenkanälen unterteilt wird. In vielen Fällen ist es nicht nur egal, ob die Hardware-Stimmen nicht perfekt aufeinander abgestimmt sind – man möchte vielleicht sogar, dass einige von ihnen etwas lauter sind als andere.

Bei der Verwendung eines polyphonen Instruments zur Wiedergabe von MIDI-Daten ist es jedoch wichtig sicherzustellen, dass sich alle Stimmen gleich verhalten, und der einfachste Weg, dies zu erreichen, besteht darin, dieselben Schaltkreise zu verwenden, um sie alle zu implementieren, typischerweise mit einer Art Wavetable-Generierung .

Es gibt viele Möglichkeiten, Wavetable-basierte Musik zu spielen, die einen Kompromiss zwischen Rechenleistung und CPU-Zeit und Speicheranforderungen eingeht. Ich habe einen vierstimmigen Wavetable-Generator für den 6502 geschrieben, der zwölf Anweisungen (46 Zyklen) für jedes Ausgangssample verwendete [durchschnittlich nur drei pro Stimme!], aber dafür Datentabellen im Wert von fast 3K benötigte. Ich habe auch einen 8-stimmigen Wavetable-Generator für einen 10-MHz-PIC und einen 16-stimmigen für den PSOC geschrieben. Ich habe nicht viel mit dem Arduino gemacht, aber ich würde vermuten, dass ein achtstimmiger Wavetable-Synthesizer funktionieren würde.

Bei der Erstellung eines Wavetable-Synthesizers ist jedoch eine wichtige Sache zu beachten: Rechteckwellen klingen oft schlecht, wenn das Timing auf etwas quantisiert wird, das kein Vielfaches ihrer Frequenz ist. Wenn Sie saubere Rechteckwellen für ein MIDI-Keyboard mit fünf Oktaven erzeugen möchten, müssen Sie möglicherweise eine ziemlich hohe Abtastrate verwenden. Wenn Sie stattdessen glattere Wellen erzeugen möchten, können Sie mit einer niedrigeren Abtastrate auskommen.

Ich habe das Arduino nicht verwendet, daher weiß ich nicht, welche Art von Konstrukten in C den besten Code ergeben würden, aber eine typische Wavetable-Implementierung auf dem ARM würde so aussehen:

uint32_t freq[8],phase[8];
int32_t volume[8];
int8_t *table[8]; // Pointer to 256-byte array
int32_t total;

total = 0;
phase[0] += freq[0]; total += table[0][phase[0] >> 8]*volume[0];
phase[1] += freq[1]; total += table[1][phase[1] >> 8]*volume[1];
...
phase[7] += freq[7]; total += table[7][phase[7] >> 8]*volume[7];

Nach dem obigen Code totalwird ein Wert gespeichert, der (falls erforderlich) skaliert und entweder an einen DAC ausgegeben oder zum Einstellen eines PWM-Tastverhältnisses verwendet werden kann.

Der obige Code geht davon aus, dass die Wavetable genau 256 Bytes lang ist; andere Ansätze können verwendet werden, wenn dies nicht der Fall ist. Wenn der Arduino die Multiplikation mit nicht effizient durchführen kann volume, ist es möglich, für jede Lautstärke eine andere Tabelle zu verwenden (mein 6502-Code verwendete zwei Sätze von Tabellen - einen für "laut" und einen für "leise").

Eine zusätzliche Überlegung, wenn Sie einen DAC verwenden (weniger zutreffend, wenn Sie einen PWM verwenden), ist, dass es hilfreich sein kann, einen Interrupt zu haben, anstatt alle für jede Welle generierten Einzelwerte zusammenzufassen und als Gruppe auszugeben geschieht achtmal so schnell und gibt jeweils den Wert für eine Welle aus. Dadurch werden effektiv drei weitere Bits an ADC-Präzision gewonnen. Wenn Sie Sinuswellen möchten und Ihre Hardware keine schnelle Multiplikation unterstützt, ist das Erzeugen von zwei Sinuswellen mit voller Amplitude, deren Frequenzen übereinstimmen, aber deren Phasen sich unterscheiden, gleichbedeutend mit dem Erzeugen einer Sinuswelle, deren Phase der Durchschnitt der beiden vollen ist -starke, und deren Amplitude proportional zur Größe "Eins plus Kosinus der Phasendifferenz" ist. Mein PIC-basierter Wellenformgenerator hat diesen Trick verwendet.

Ich bin auf einem Tablet, also muss ich mich kurz fassen. Um mehrere Töne zu haben, benötigen Sie eine Struktur für jede Stimme in Ihrem polyphonen Instrument, die die Ton-MIDI-Notennummer enthält, eine Konstante, die den Ausgangspin enthält, einen "Phasenakkumulator" für den Ton und ein Winkelinkrement für die Phase Akkumulator. Sie initialisieren diese Strukturen dann mit den gewünschten Ausgangspins und anderen leeren Feldern und speichern sie in einer Art Datenstruktur (ein Stapel wäre gut). Dies ist dann der "unbenutzte Notenstapel".

Wenn eine MIDI-Note-On-Nachricht eingeht, rufen Sie eine Funktion auf, um eine unbenutzte Note aus dem Stapel zu entfernen, und verwenden die Notennummer und eine Nachschlagetabelle, um das entsprechende Winkelinkrement für den Phasenakkumulator zu finden. Dann wird die ausgefüllte Struktur in einen leeren Slot in einem Array namens "Spielnoten-Array" eingefügt. Wenn eine Note-Off-Nachricht eingeht, wird eine Funktion aufgerufen, die das Array durchsucht, um die ausgeschaltete Note zu finden, und die Struktur aus dem Array entfernt und sie zurück in den unbenutzten Notenstapel legt.

Es wird auch einen Timer-Interrupt geben, der mit einer bestimmten Rate aufgerufen wird, abhängig von der Rechteckwelle mit der höchsten Ausgangsfrequenz, die Sie benötigen. Die Inkremente des Phasenakkumulators werden basierend auf dieser Rate berechnet. In der Unterbrechung wird das Array von Spielnoten durchsucht und alle Phasenakkumulatoren aktualisiert. Um die Rechteckwellen zu erzeugen, werden die Ausgangspins niedrig gesetzt, wenn das oberste Byte des jeweiligen Phasenakkumulators kleiner als 128 ist, hoch, wenn es größer ist.

Bearbeiten: Um die Dinge schneller zu machen, könnten Sie zur Laufzeit einfach eine Reihe von Strukturen für jede Stimme mallocieren. Verwenden Sie dann die Zeiger, anstatt sich um die Strukturen selbst zu bewegen.

Was Sie mit den Strukturen beschreiben, ist genau die Funktionsweise der Tonbibliothek. Sie initialisieren einen "Ton" mit einer Pin-Nummer (was bedeutet, dass ich auf 6 Töne beschränkt bin, da ich 6 verfügbare Pins habe), und dann können Sie eine Note an die Tonstruktur senden, die sie mit Tone einschaltet. spielen(). Sie können den Ton dann mit Tone.stop() ausschalten. Der schwierige Teil besteht jedoch darin, den richtigen Ton zu indizieren, um die angegebene Note auszuschalten. Das ist eine der Straßensperren, auf die ich immer wieder gestoßen bin. Ich kann einen Ton einschalten, aber wenn ich eine Note-Off-Nachricht erhalte, weiß ich nicht, wie ich sagen soll, welcher Ton gestoppt werden soll
@derstrom8 In der Dokumentation für die Tonbibliothek heißt es, dass jeweils nur ein Ton abgespielt werden kann - "Es kann immer nur ein Ton erzeugt werden. Wenn ein Ton bereits auf einem anderen Pin abgespielt wird, wird der Aufruf totone() haben keine Auswirkung. Wenn der Ton auf demselben Pin abgespielt wird, wird der Anruf seine Frequenz festlegen."
Wenn Sie meinen ursprünglichen Beitrag noch einmal lesen, werden Sie feststellen, dass ich nicht die Standardtonbibliothek verwende. Ich verwende einen ganz anderen, der polyphone Klänge unterstützt. Ich habe es sogar im Beitrag verlinkt.
@derstrom8 Tut mir leid! Ich habe irgendwie übersehen, dass Sie die nicht native Bibliothek verwendet haben. Nun, dann sollte es einfacher sein. Ich werde versuchen, diese Antwort bei Gelegenheit zu bearbeiten ...
@derstrom8 Grundsätzlich sollte die Idee mit dem Stapel von Strukturzeigern funktionieren - Sie speichern einfach eine konstante Pin-Nummer in jeder Struktur zusammen mit einer anfänglich leeren Notennummer. Wenn dann eine Note ankommt, schalten Sie die Note ein, ordnen eine Notennummer einem Pin zu, indem Sie die Notennummer in der Struktur speichern, und bewegen den Strukturzeiger in das Array "playing notes". Wenn dann eine Note aus eintrifft, scannen Sie die Strukturen, auf die die Zeiger im Array zeigen, und finden die Struktur mit der richtigen Note, verwenden den zugehörigen Stift, um sie auszuschalten, und bewegen dann den Zeiger zurück zum Stapel.
@derstrom8 Wenn Sie dieselbe Note mehrmals ausgelöst haben, ist ein wenig zusätzliche Arbeit erforderlich, um den "richtigen" Pin zum Ausschalten mit diesem Schema zu erhalten - z. B. das Hinzufügen eines Felds zur Struktur, um anzugeben, in welcher Reihenfolge die Noten ausgelöst wurden, und das Überprüfen wenn Notizen hinzugefügt und entfernt werden.
Keine Sorge @Bitrex! Das sind tolle Ideen, die werde ich mir auf jeden Fall anschauen. Ich habe noch nie viel mit Strukturen gemacht, aber hey – es ist nie zu spät, es zu lernen! Danke noch einmal!
@derstrom8 Ich habe mir diese Frage gerade noch einmal angesehen und festgestellt, dass jeder Ton eine Anzeigemethode hat. Das sollte die Sache erleichtern...
Hallo @Bitrex, ich habe mir die isPlaying-Methode angesehen und einige Experimente durchgeführt, aber ich habe festgestellt, dass die Tonbibliothek nicht so funktioniert, wie ich es brauche. Ich muss die Einschaltzeit jedes Impulses, aus dem die Note besteht, auf weniger als 48 uS begrenzen, um zu verhindern, dass mein Schaltkreis überhitzt. Ich habe mich dafür entschieden, stattdessen Timer und Bresenham-Akkumulatoren zu verwenden. Ich habe derzeit einen funktionierenden Code, der drei Töne gleichzeitig liefern kann (ich habe erfolgreich einen schönen C-Akkord gespielt), aber ich muss das MIDI noch zum Laufen bringen - die Noten bleiben hängen.