Script en Python para Armonizar en Tiempo Real

Epiphone
por hace 14 horas
Hola Hispasonicos, con la ayuda de Gemini he creado un script ahorrándome mucho trabajo. Esta diseñado para Ubuntu Studio pero quizas con alguna modificación sirva para otros sistemas operativos, paso a detallar cual su función, ese resumen también lo ha redactado Gemini, soy algo vago :-D



# 🎹 Presentación: Armonizador MIDI Dinámico de Tríadas en Cascada

### ¿Qué es este script?

Es una herramienta en Python que actúa como un **asistente de armonización inteligente en tiempo real**. El usuario solo tiene que tocar una línea melódica (una sola nota a la vez) y el script genera automáticamente un acompañamiento rico de tres voces (tríadas), asegurándose de que **ninguna nota suene fuera de la escala elegida**.

---

### 🚀 Las 3 Características Clave

#### 1. Inteligencia Diatónica (Se adapta a cualquier escala)

Al arrancar el script, el usuario introduce la escala mayor en la que quiere tocar (ej: *Do Mayor, Fa# Mayor, Mi Bemol Mayor*).

* Si el músico toca una nota dentro de la escala, el script la armoniza.
* Si por error toca una nota fuera de la escala (una alteración accidental), el script la detecta y la deja pasar "limpia" sin armonizar para evitar disonancias desagradables.

#### 2. La Melodía manda (Criterio de Voz Aguda)

En muchos armonizadores comerciales, las voces añadidas quedan por encima de lo que tocas, ensuciando la melodía. En este script, **la nota que tú tocas siempre es la más aguda (la voz principal)**. El script calcula los intervalos matemáticos necesarios para construir el acorde físicamente **por debajo** de tu dedo, logrando un sonido de acompañamiento robusto y natural.

#### 3. Variación de Acordes Dinámica (Ciclo de 3 Pasos)

Para evitar que el acompañamiento suene monótono, el script no repite siempre el mismo molde. Cada nota de una escala pertenece exactamente a tres acordes diferentes. El script aprovecha esto y, **cada vez que pulsas una nota, alterna cíclicamente entre esos 3 acordes**:

* **Pulsación 1:** Crea un intervalo de 3ª y 6ª inferior (Ej. tocando Do: genera *Do - La - Mi* ➔ **La menor**).
* **Pulsación 2:** Crea un intervalo de 3ª y 5ª inferior (Ej. tocando Do: genera *Do - La - Fa* ➔ **Fa Mayor**).
* **Pulsación 3:** Crea un intervalo de 4ª y 6ª inferior (Ej. tocando Do: genera *Do - Sol - Mi* ➔ **Do Mayor**).

---

### 🛠️ ¿Cómo funciona bajo el capó? (Para los interesados en la técnica)

1. **Conexión MIDI Flexible:** Utiliza la librería `mido` para detectar cualquier teclado controlador de entrada y conectarse a cualquier sintetizador o DAW (como Ableton, Logic o FL Studio) como salida.
2. **Cuidado del "Note Off":** El script realiza un seguimiento en tiempo real de qué notas extra ha creado para cada pulsación. Cuando el usuario levanta el dedo del teclado, el script apaga instantáneamente todo el bloque de notas para evitar que los acordes se queden "pegados" o se mezclen entre sí.

> **Resumen para la audiencia:** Es un script que transforma líneas melódicas simples en progresiones de acordes en cascada sobre la marcha, ideal para experimentar con texturas armónicas en directo o inspirarse en el proceso de composición.



Os Adjunto un Audio Ejemplo
Archivos adjuntos ( para descargar)
🎵 TriadasCromaticas.mp3
Epiphone
por hace 14 horas
#1 Se me olvido adjuntar el script....perdón
Parece que no deja adjuntar el archivo  

import sys
import mido

# --- CONFIGURACIÓN DINÁMICA DE ESCALAS ---
DICCIONARIO_NOTAS = {
    "C": 0, "C#": 1, "Db": 1, "D": 2, "D#": 3, "Eb": 3, "E": 4,
    "F": 5, "F#": 6, "Gb": 6, "G": 7, "G#": 8, "Ab": 8, "A": 9,
    "A#": 10, "Bb": 10, "B": 11
}

INTERVALOS_MAYOR = [2, 2, 1, 2, 2, 2, 1]

def generar_escala_mayor(tonica_str):
    """Genera los 7 semitonos de la escala mayor a partir de una tónica."""
    nota_raiz = DICCIONARIO_NOTAS.get(tonica_str.strip())
    if nota_raiz is None:
        print(f"Nota '{tonica_str}' no válida. Usando 'C' (Do) por defecto.")
        nota_raiz = 0
   
    escala = []
    nota_actual = nota_raiz
    for intervalo in INTERVALOS_MAYOR:
        escala.append(nota_actual % 12)
        nota_actual += intervalo
   
    return sorted(escala)

def obtener_nota_en_escala(nota_midi):
    """Devuelve la nota base (0-11) y la octava de una nota MIDI."""
    return nota_midi % 12, nota_midi // 12

def buscar_nota_diatonica_inferior(nota_midi, escala_mayor, grado_descendente):
    """
    Calcula una nota diatónica hacia abajo en la escala.
    grado_descendente=3 significa una tercera hacia abajo (bajar 2 posiciones en la lista).
    """
    nota_base, octava = obtener_nota_en_escala(nota_midi)
    if nota_base not in escala_mayor:
        return None
       
    idx = escala_mayor.index(nota_base)
    idx_armonia = idx - (grado_descendente - 1)
   
    # Ajuste automático de octavas inferiores en Python
    nueva_octava = octava + (idx_armonia // 7)
    nuevo_idx_escala = idx_armonia % 7
   
    return (nueva_octava * 12) + escala_mayor[nuevo_idx_escala]

def seleccionar_puerto(lista_puertos, tipo_puerto):
    if not lista_puertos:
        print(f"No se detectaron puertos de {tipo_puerto} disponibles.")
        sys.exit(1)
    print(f"\n--- Puertos de {tipo_puerto} disponibles ---")
    for i, puerto in enumerate(lista_puertos):
        print(f"[{i}] {puerto}")
    while True:
        try:
            seleccion = int(input(f"Selecciona el número ({tipo_puerto}): "))
            if 0 <= seleccion < len(lista_puertos):
                return lista_puertos[seleccion]
        except ValueError:
            pass

def obtener_intervalos_descendentes(estado):
    """
    Devuelve los grados diatónicos descendentes desde la nota más aguda.
    - Estado 0: C -> A (3ª inf) -> E (6ª inf)
    - Estado 1: C -> A (3ª inf) -> F (5ª inf)
    - Estado 2: C -> G (4ª inf) -> E (6ª inf)
    """
    if estado == 0:
        return [3, 6], "Melodía + 3ª inf + 6ª inf (Ej: C A E)"
    elif estado == 1:
        return [3, 5], "Melodía + 3ª inf + 5ª inf (Ej: C A F)"
    else:
        return [4, 6], "Melodía + 4ª inf + 6ª inf (Ej: C G E)"

def main():
    print("Iniciando Armonizador de Melodía Aguda (3 Pasos Descendentes)...")
   
    # 1. Selección de Escala
    tonica_seleccionada = input("Introduce la tónica de la Escala Mayor (Ej: C, F#, Bb): ").upper()
    if tonica_seleccionada not in DICCIONARIO_NOTAS:
        tonica_seleccionada = "C"
       
    NOTAS_ESCALA_MAYOR = generar_escala_mayor(tonica_seleccionada)
   
    # 2. Selección de Puertos MIDI
    puertos_entrada = mido.get_input_names()
    puertos_salida = mido.get_output_names()
   
    nombre_entrada = seleccionar_puerto(puertos_entrada, "ENTRADA")
    nombre_salida = seleccionar_puerto(puertos_salida, "SALIDA")
   
    print(f"\nSistema listo en {tonica_seleccionada} MAYOR.")
    print("La nota que toques SIEMPRE será la más aguda. Armonizaciones por debajo:")
    print("  Pulsación 1 -> 3ª inf y 6ª inf (Ej con C: C A E)")
    print("  Pulsación 2 -> 3ª inf y 5ª inf (Ej con C: C A F)")
    print("  Pulsación 3 -> 4ª inf y 6ª inf (Ej con C: C G E)")
    print("Presiona Ctrl+C para salir.\n")
   
    contador_armonias = 0     
    notas_armonizadas_activas = {}

    try:
        with mido.open_input(nombre_entrada) as puerto_in, mido.open_output(nombre_salida) as puerto_out:
            for mensaje in puerto_in:
               
                # === GESTIÓN DE NOTA PRESIONADA (NOTE ON) ===
                if mensaje.type == 'note_on' and mensaje.velocity > 0:
                   
                    # Filtro de escala: Si no está en la escala, pasa sola sin armonizar
                    if buscar_nota_diatonica_inferior(mensaje.note, NOTAS_ESCALA_MAYOR, grado_descendente=1) is None:
                        puerto_out.send(mensaje)
                        print(f"Nota: {mensaje.note} -> Fuera de escala (Pasa libre).")
                        continue

                    # Enviamos la nota original (Voz 1 - La más aguda)
                    puerto_out.send(mensaje)
                   
                    # Determinar el patrón según el orden cíclico (0, 1, 2)
                    estado_actual = contador_armonias % 3
                    intervalos, nombre_patron = obtener_intervalos_descendentes(estado_actual)
                   
                    # Calcular las dos voces inferiores SIEMPRE partiendo de la nota original
                    voz_2 = buscar_nota_diatonica_inferior(mensaje.note, NOTAS_ESCALA_MAYOR, intervalos[0])
                    voz_3 = buscar_nota_diatonica_inferior(mensaje.note, NOTAS_ESCALA_MAYOR, intervalos[1])
                   
                    notas_triada = []
                   
                    # Validar y enviar Voz 2
                    if voz_2 and 0 <= voz_2 <= 127:
                        notas_triada.append(voz_2)
                        puerto_out.send(mensaje.copy(note=voz_2))
                       
                    # Validar y enviar Voz 3
                    if voz_3 and 0 <= voz_3 <= 127:
                        notas_triada.append(voz_3)
                        puerto_out.send(mensaje.copy(note=voz_3))
                       
                    if notas_triada:
                        notas_armonizadas_activas[mensaje.note] = notas_triada
                       
                    print(f"🎵 [Pulsación #{estado_actual + 1}] Nota Aguda: {mensaje.note} -> Acorde: {notas_triada} [{nombre_patron}]")
                   
                    # Avanzar el ciclo
                    contador_armonias += 1
                       
                # === GESTIÓN DE NOTA SOLTADA (NOTE OFF) ===
                elif mensaje.type == 'note_off' or (mensaje.type == 'note_on' and mensaje.velocity == 0):
                    puerto_out.send(mensaje)
                   
                    if mensaje.note in notas_armonizadas_activas:
                        lista_notas_a_apagar = notas_armonizadas_activas.pop(mensaje.note)
                        for nota_extra in lista_notas_a_apagar:
                            puerto_out.send(mensaje.copy(note=nota_extra))
               
                else:
                    puerto_out.send(mensaje)
                   
    except KeyboardInterrupt:
        print("\n\nArmonizador apagado.")

if __name__ == "__main__":
    main()
Nuevo post

Regístrate o para poder postear en este hilo

Música
Temas