MIDI a onda cuadrada polifónica en Arduino

He estado luchando con esto durante unos días y realmente me gustaría continuar con mi proyecto. Desafortunadamente esto me está retrasando. Lo que estoy tratando de hacer es recuperar señales MIDI enviadas a través de USB a Arduino (Uno), decodificarlas y luego usar una biblioteca de tonos no nativa ( https://code.google.com/p/rogue-code/wiki /ToneLibraryDocumentación) para reproducir varios tonos de onda cuadrada a la vez. Me quedan 6 puertos de E/S en mi Arduino para trabajar, así que espero que eso me limite a 6 tonos. He intentado varios métodos diferentes para hacer que esto funcione, pero nunca he podido terminar ninguno de ellos debido a que me encontré con un obstáculo de una forma u otra. Mi esperanza es tocar varios tonos, ya sean del mismo canal (acordes) o de diferentes canales (instrumentos separados), pero parece que todavía no puedo encontrar una buena manera de hacerlo.

Leer los datos MIDI no es la parte difícil. Tengo el siguiente código, escrito por Greg Kennedy y publicado en los foros de Arduino ( http://forum.arduino.cc/index.php?topic=79326.0 ), para recuperar datos de un canal MIDI específico y reproducirlo usando el nativo tone(), pero lamentablemente solo puede tocar una nota a la vez.

// 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
    }
  }
}

Este código funciona muy bien para audio monofónico, pero realmente necesito producir tonos polifónicos. Siempre he sido más un tipo de PIC, por lo que Arduino todavía es bastante nuevo para mí. ¡Gracias chicos!

EDITAR: Para que quede claro, quiero convertir una entrada MIDI multitono en una salida de onda cuadrada multitono, que usaré como señal de interrupción para otra parte del proyecto.

La síntesis de tabla de ondas es mucho más fácil en hardware de gama baja. Ni siquiera bromeando.
No estoy familiarizado con MIDI + Wavetable Synth con Arduino. ¿Tendrías un enlace a una buena fuente de datos y ejemplos? Lo busqué en Google pero no encontré ninguna documentación extensa al respecto, solo algunos pequeños proyectos aquí y allá.
No he visto uno. Pero se puede hacer desde cero más fácilmente que mezclar ondas cuadradas.
La salida debe ser una onda cuadrada, ya que servirá como interruptor para la siguiente parte del proyecto. Supongo que no estoy lo suficientemente familiarizado con la síntesis de tabla de ondas para entender cómo podría aplicarse a este proyecto. Aunque gracias por la sugerencia, lo miraré.
Oh. Pensé que estabas usando el Arduino para generar audio. Olvidalo entonces.
Lo siento, fue mi culpa que no lo aclaré. Editaré la publicación original.
Si no te importa, ¿puedes mencionar cuál es la próxima parte del proyecto? Tener más información puede ayudarnos a guiarlo por un camino diferente o darle una mejor sugerencia. También estoy trabajando en un proyecto midi (archivo) para arduino ... Aunque en mi caso, todo el cálculo midi se realiza en una computadora y se envía al arduino a través de una serie.
De nada. Lo usaré para modular la señal de transmisión a una bobina Tesla de estado sólido. No lo mencioné porque no creo que ayude mucho: la señal simplemente se usará para habilitar/deshabilitar (repetidamente) un chip UCC27425 Gate Drive
Por cierto, debería haber mencionado cómo estoy enviando la señal al Arduino. Estoy usando el puente Hairless MIDI-Serial, por lo que el MIDI se envía al Arduino a través de la conexión USB

Respuestas (2)

Reproducir música polifónica mediante el uso de circuitos separados para cada "voz" funciona muy bien cuando la música se subdivide lógicamente en varios canales de una sola voz. En muchos casos, a uno no solo no le importará si las voces del hardware no coinciden perfectamente, sino que incluso puede querer que algunas de ellas suenen un poco más fuertes que otras.

Sin embargo, cuando se usa un instrumento polifónico para reproducir datos MIDI, es importante asegurarse de que todas las voces se comporten de manera equivalente, y la forma más fácil de lograrlo es usar el mismo circuito para implementarlas todas, generalmente con algún tipo de generación de tablas de ondas. .

Hay muchas formas de interpretar música basada en tablas de ondas, que compensan la sofisticación por el tiempo de CPU y los requisitos de memoria. Escribí un generador de tabla de ondas de cuatro voces para el 6502 que usaba doce instrucciones (46 ciclos) para cada muestra de salida [¡un promedio de solo tres por voz!] pero requería casi 3K de tablas de datos. También he escrito un generador de tablas de ondas de 8 voces para un PIC de 10 MHz y uno de 16 voces para el PSOC. No he hecho mucho con el Arduino, pero supongo que un sintetizador de tabla de ondas de ocho voces sería viable.

Sin embargo, hay una cosa importante a tener en cuenta cuando se hace un sintetizador de tabla de ondas: las ondas cuadradas a menudo suenan mal si el tiempo se cuantifica en algo que no es un múltiplo de su frecuencia. Si desea generar ondas cuadradas limpias para un teclado MIDI de cinco octavas, es posible que deba utilizar una frecuencia de muestreo bastante alta. Si desea generar ondas más suaves, puede arreglárselas con una frecuencia de muestreo más baja.

No he usado Arduino, por lo que no sé qué tipo de construcciones en C generarían el mejor código, pero una implementación típica de tabla de ondas en ARM se vería así:

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];

Después del código anterior, totaltendrá un valor que puede escalarse (si es necesario) y enviarse a un DAC o usarse para establecer un ciclo de trabajo PWM.

El código anterior asume que la tabla de ondas tiene exactamente 256 bytes de largo; se pueden usar otros enfoques si ese no es el caso. Si el Arduino no puede realizar la multiplicación de manera eficiente por volume, es posible usar una tabla diferente para cada nivel de volumen (mi código 6502 usaba dos juegos de tablas, uno para "alto" y otro para "suave").

An additional consideration if you're using a DAC (less applicable if you're using a PWM) is that rather than summing together all the individual values generated for each wave and outputting them as a group, it may be helpful have an interrupt which happens eight times as fast, and outputs the value for one wave each time. Doing that will effectively gain three more bits of ADC precision. Also, if you happen to want sine waves and your hardware doesn't support fast multiplication, generating two full-amplitude sine waves whose frequencies match but whose phases differ, is equivalent to generating one sine wave whose phase is the average of the two full-strength ones, and whose amplitude is proportional to the quantity "one plus the cosine of the phase difference". My PIC-based waveform generator used that trick.

Estoy en una tableta, así que tendré que ser un poco breve. Para tener múltiples tonos, querrá una estructura para cada voz en su instrumento polifónico que contenga el número de nota MIDI del tono, una constante que contenga el pin de salida, un "acumulador de fase" para el tono y un incremento de ángulo para la fase. acumulador. Luego inicializa estas estructuras con los pines de salida que desea y otros campos vacíos y los almacena en algún tipo de estructura de datos (una pila sería buena). Esta es entonces la "pila de notas no utilizadas".

Cuando entra un mensaje de activación de nota MIDI, llama a una función para sacar una nota no utilizada de la pila y utiliza el número de nota y una tabla de búsqueda para encontrar el incremento de ángulo apropiado para el acumulador de fase. Luego, la estructura completa se colocará en una ranura vacía en una matriz llamada "matriz de notas tocadas". Cuando aparece un mensaje de nota desactivada, se llama a una función que examina esa matriz para encontrar la nota que se apagó, elimina la estructura de la matriz y la vuelve a colocar en la pila de notas no utilizadas.

También habrá una interrupción del temporizador llamada a cierta velocidad dependiendo de la onda cuadrada de frecuencia de salida más alta que necesite. Los incrementos del acumulador de fase se calcularán en base a esta tasa. En la interrupción, se escaneará la matriz de notas que se están tocando y se actualizarán todos los acumuladores de fase. Para generar las ondas cuadradas, los pines de salida se establecerán en bajo cuando el byte superior del acumulador de fase respectivo sea menor que 128, alto cuando sea mayor.

Editar: para hacer las cosas más rápidas, podría simplemente hacer un malloc de un montón de estructuras para cada voz en tiempo de ejecución. Luego use los punteros en lugar de moverse alrededor de las estructuras.

Lo que describe con las estructuras es exactamente cómo funciona la biblioteca de tonos. Inicializa un "Tono" con un número de pin (lo que significa que estoy limitado a 6 tonos, ya que tengo 6 pines disponibles), y luego puede enviar una nota a la estructura de tono, y la enciende usando Tono. jugar(). Luego puede apagar el tono usando Tone.stop(). Sin embargo, la parte difícil es indexar el tono correcto para desactivar la nota especificada. Ese es uno de los obstáculos con los que seguí tropezando. Puedo activar un tono, pero cuando recibo un mensaje de nota desactivada, no sé cómo decirle qué tono detener
@derstrom8 La documentación de la biblioteca de tonos establece que solo se puede reproducir un tono a la vez: "Solo se puede generar un tono a la vez. Si un tono ya se está reproduciendo en un pin diferente, la llamada a tono () tendrá ningún efecto. Si el tono se reproduce en el mismo pin, la llamada establecerá su frecuencia".
Si vuelve a leer mi publicación original, verá que no estoy usando la biblioteca de tonos estándar. Estoy usando uno completamente diferente, que admite sonidos polifónicos. Incluso lo vinculé en la publicación.
@derstrom8 ¡Lo siento! De alguna manera me perdí que estabas usando la biblioteca no nativa. Bueno, entonces debería ser más simple. Intentaré editar esta respuesta cuando tenga la oportunidad ...
@derstrom8 Básicamente, la idea con la pila de punteros de estructura debería funcionar: simplemente almacene un número de pin constante en cada estructura, junto con un número de nota inicialmente vacío. Luego, cuando entra una nota, enciende la nota, asocia un número de nota con un alfiler almacenando el número de nota en la estructura y mueve el puntero de estructura a la matriz de "tocar notas". Luego, cuando aparece una nota apagada, escanea las estructuras a las que apuntan los punteros en la matriz y encuentra la estructura que tiene la nota correcta, usa el pin asociado para apagarla y luego mueve el puntero de regreso a la pila.
@derstrom8 Si tiene la misma nota activada varias veces, para que el pin "correcto" se apague con este esquema necesitará un poco de trabajo adicional, como agregar un campo a la estructura para indicar en qué orden se activaron las notas y verificar esto cuando se agregan y eliminan notas.
¡No te preocupes @Bitrex! Estas son algunas ideas geniales, así que definitivamente las miraré. Nunca antes hice mucho con las estructuras, pero oye, ¡nunca es demasiado tarde para aprender! ¡Gracias de nuevo!
@derstrom8 Estaba mirando esta pregunta nuevamente y noté que cada tono tiene un método de reproducción. Eso debería facilitar las cosas...
Hola @Bitrex, investigué el método isPlaying y experimenté un poco, pero determiné que la biblioteca de tonos no funcionará de la manera que necesito. Necesito limitar el tiempo de encendido de cada pulso que compone la nota a menos de 48uS para evitar que mi circuito se sobrecaliente. He optado por usar temporizadores en su lugar y acumuladores Bresenham. Actualmente tengo un código de trabajo que puede proporcionar tres tonos a la vez (toqué con éxito un buen acorde C), pero aún tengo que hacer que el MIDI funcione, las notas siguen pegándose.