Saltearse al contenido

Módulo 2.1: Memoria Persistente, Estructuras de Datos (`[table]`, `[coll]`, `[dict]`) y Manejo de Estado

“Un algoritmo sin memoria es solo una función reactiva; con memoria se convierte en un sistema musical vivo.”
— Robert Rowe, Machine Musicianship


1. Fundamento Teórico: Los Tres Niveles de Persistencia en Computación Musical

(Inspirado en Todd Winkler, MIT Press y Cipriani & Giri, Vol. 2)

En la interacción en tiempo real, un sistema musical no puede depender únicamente de los datos que viajan volátiles por los cables. Necesitamos almacenar tres categorías de información:

FIG 2.0 · Jerarquía de Memoria y Persistencia en Max


2. Objetos de Almacenamiento Clásicos: [value], [table] y [coll]

A. Memoria Global Compartida: [value] ([v])

  • Almacena un único valor (número o lista corta) asociado a un nombre global inmutable.
  • [v mi_tempo]: Cualquier objeto en cualquier subpatch o ventana que se llame [v mi_tempo] comparte el mismo puntero de memoria. Modificarlo en un extremo lo actualiza instantáneamente en el otro sin tirar cables.

B. Arreglos Indexados: [table]

  • Almacena pares (índice, valor) de números enteros.
  • Posee una ventana gráfica nativa para dibujar curvas de transferencia matemática (curvas de respuesta de pedales, mapeos de sensores, tablas armónicas).
  • Acceso O(1)O(1): Enviar un índice al inlet izquierdo devuelve el valor correspondiente en un solo ciclo de reloj.

C. La Base de Datos Clásica: [coll] (Collection)

  • Es la navaja suiza histórica de Max para composiciones complejas.
  • Cada fila tiene una clave única (número o símbolo) y un vector de datos.

FIG 2.0B · Estructuras de Persistencia: Registros Planos [coll] vs. Jerarquía [dict] (JSON)

  • Admite modos de búsqueda directa, lectura secuencial automática (next, prev), ordenamiento por clave o por valores, y persistencia directa en archivos de texto legibles.

3. El Estándar Moderno: Árboles Jerárquicos con [dict] y JSON

En aplicaciones de gran escala (como Max for Live o sintetizadores con cientos de parámetros), las listas planas de [coll] se quedan cortas.

El ecosistema [dict] de Cycling ‘74 introduce estructuras de datos basadas en JSON nativo dentro de Max:

  • Permite crear diccionarios anidados:
    {
    "preset_name": "Ambient Pad 01",
    "filter": {
    "cutoff": 1850.5,
    "resonance": 0.72,
    "mode": "lowpass"
    },
    "lfo": {
    "rate": 0.25,
    "depth": 0.6
    }
    }

La Sintaxis de Rutas (Dot-Notation y Slash-Notation):

Para consultar o modificar datos dentro de [dict], no necesitas recorrer el árbol manualmente. Usas rutas:

  • get filter::cutoff ──► Devuelve 1850.5.
  • set filter::resonance 0.85 ──► Modifica el valor sin tocar el resto del árbol.

4. Bajo el Capó: Diccionarios en C y Paso por Referencia (dict.route.c)

Mirando las entrañas del Max SDK en sources/max-sdk/source/dictionary/dict.route/dict.route.c:

El Problema de la Clonación de Memoria

Si un diccionario contiene 10.000 parámetros o la partitura entera de una sinfonía, copiar todo el JSON de un objeto a otro a través de un cable congelaría el hilo de audio.

La Solución de Cycling ‘74: Paso por Nombre Registrado

En C, Max no envía el contenido del diccionario por el cable; envía únicamente el puntero al símbolo del diccionario registrado:

void dict_route_dictionary(t_dict_route* x, t_symbol* s) {
// Busca el diccionario en la tabla maestra de memoria global
t_dictionary* d = dictobj_findregistered_retain(s);
if (!d) {
object_error((t_object*)x, "unable to reference dictionary named %s", s);
return;
}
}

FIG 2.1 · Gestión de Pointers en RAM y Sintaxis JSON


Comportamientos Clave en la Gestión de Instancias: Nombres Efímeros y Diccionarios Anónimos

Un factor crítico en el ciclo de vida de los datos estructurados en Max se manifiesta en el mensaje de error: "unable to reference dictionary named u123456789".

Mecanismos subyacentes y pautas de diseño:

  1. Diccionarios Anónimos vs. Nombrados:
    • Al instanciar un [dict] sin argumentos, Max le asigna un identificador efímero generado dinámicamente (u seguido de un valor hash único).
    • Si dicho sub-árbol se crea de forma transitoria o se interrumpe la referencia, el objeto es recolectado por el gestor de memoria interno, provocando que cualquier [dict.unpack] o [dict.view] aguas abajo falle al intentar acceder a un puntero ya invalidado.
    • Criterio de diseño: En arquitecturas de producción, se debe asignar siempre un identificador explícito a los diccionarios principales ([dict mi_sintetizador]).
  2. Consultas Directas (get) vs. Desempaquetado (dict.unpack):
    • Para consultar claves anidadas en profundidad (por ejemplo, filter::resonance), el enfoque óptimo consiste en despachar mensajes get path::to::key directos en lugar de encadenar múltiples [dict.unpack]. Este método reduce la sobrecarga de despacho gráfico y no requiere instanciar receptores intermedios.
  3. Manejo de Arreglos de Diccionarios:
    • [dict.unpack] no procesa de forma plana una lista de objetos JSON [ { "id": 1 }, { "id": 2 } ].
    • Se debe iterar formalmente mediante [dict.iter] o direccionar por índice explícito: get features[0]::id.

Principio Arquitectónico: Cuando conectas un cable entre objetos dict, viaja un puntero de 8 bytes, no megabytes de datos. La manipulación de árboles gigantes en Max es instantánea y de costo cero.

Fundamentación Teórica (Federico Schumacher — Tesis Doctoral AMI, UNAM/CMMAS): En su investigación sobre la calificación de datos simbólicos, Schumacher formaliza cómo la representación jerárquica en diccionarios y matrices (dict y coll) resulta indispensable para almacenar no solo los eventos de control brutos (alturas, velocidades y duraciones), sino los descriptores de Music Information Retrieval (MIR) asociados: entropía melódica, perfiles de contorno y coeficientes de similitud cronotónica. Esta arquitectura de memoria desacoplada permite calificar y re-ordenar clips generativos en tiempo real sin interferir con el Scheduler de reproducción.


4 Escenarios de la Vida Real (Casos de Estudio)

Abre el parche interactivo: book/patches/modulo-02/laboratorio_04_persistencia.maxpat

Escenario 1: Tabla de Escalamiento No Lineal de Sensibilidad con [table]

  • El Problema: La respuesta de la velocidad de las teclas de un controlador suele ser lineal (0 a 127), pero la audición humana del volumen es logarítmica. Tocando suave casi no se oye y tocando medio ya suena muy fuerte.
  • La Solución: Una curva de transferencia en [table]. Mapeamos la entrada lineal a una curva exponencial dibujada a mano. El acceso es en O(1)O(1) sin consumo de CPU.

Escenario 2: Secuenciador Basado en Base de Datos con [coll]

  • El Problema: Necesitamos almacenar una secuencia polifónica compleja con notas, velocidades y duraciones, y poder reproducirla hacia adelante, hacia atrás o saltar a compases específicos.
  • La Solución: [coll] con claves numéricas indexadas. Con el mensaje next recorremos la partitura paso a paso, y con un número saltamos a cualquier evento de inmediato.

Escenario 3: Gramáticas Musicales y Modelado de Markov con [dict]

(Inspirado en Robert Rowe, Machine Musicianship, y Todd Winkler, Composing Interactive Music).

  • El Problema: Al construir un agente de improvisación armónica, las tablas planas no permiten representar probabilidades condicionales dependientes del acorde anterior ni almacenar jerarquías complejas de transiciones.
  • La Solución: Representar el autómata probabilístico en un [dict] jerárquico:
{
"tónica": {
"alturas": [60, 64, 67],
"probabilidades": { "subdominante": 0.35, "dominante": 0.55, "tónica": 0.10 }
},
"dominante": {
"alturas": [67, 71, 74],
"probabilidades": { "tónica": 0.85, "subdominante": 0.15 }
}
}
  • Max consulta los pesos de transición directamente en O(1)O(1) sin cables: get tónica::probabilidades::dominante
  • La persistencia se comparte instantáneamente entre polifonías [poly~], scripts [js] y el motor de síntesis sin duplicar estructuras en memoria RAM.

Escenario 4: Base de Datos de Presets Estructurados con [dict]

  • El Problema: Un sintetizador tiene 4 módulos (Oscilador, Filtro, LFO, Efectos). Guardar los parámetros en variables sueltas hace imposible exportar o compartir presets.
  • La Solución: [dict] con jerarquía de claves. Un solo botón write exporta el estado completo a un archivo .json en disco legible por humanos y compatible con cualquier aplicación externa.

Escenario 4: Modificación Parcial con Rutas Anidadas (dict.unpack y get)

  • El Problema: Solo queremos alterar la frecuencia de corte del filtro sin recargar ni alterar los otros 50 parámetros del sintetizador.
  • La Solución: Enviar el mensaje set filter::cutoff 800 directamente al [dict]. La memoria interna se actualiza limpiamente sin mutaciones destructivas colaterales.

3 Ejercicios Prácticos de Laboratorio

Realiza estos ejercicios en tu copia de Max utilizando el parche laboratorio_04_persistencia.maxpat:

Ejercicio 1: El Cuantizador de Escalas con [table]

  • Objetivo: Construye un corrector de afinación MIDI.
  • Desafío: Llena una tabla de 128 posiciones donde cada nota cromática entrante se redirija a la nota de la escala mayor más cercana (ej. si entra 61, devuelve 60 o 62).
  • Requisito: Al tocar cualquier teclado MIDI desordenado, la salida debe sonar 100% diatónica en tiempo real.

Ejercicio 2: Grabador / Reproductor de Eventos en Vivo con [coll]

  • Objetivo: Graba una secuencia de notas improvisada por el usuario con sus marcas de tiempo exactas.
  • Desafío: Al presionar “Record”, utiliza [timer] para medir el delta entre notas y guárdalas en [coll] con formato índice, nota vel delta;. Al presionar “Play”, reproduce la secuencia con el timing exacto usando [pipe].

Ejercicio 3: Serializador JSON de Sesión con [dict]

  • Objetivo: Construye un panel de administración de usuario que guarde: nombre del artista, BPM actual, escala seleccionada y volumen maestro.
  • Desafío: Implementa botones para “Guardar en Disco” (write session.json) y “Cargar desde Disco” (read session.json), y verifica que al reabrir el parche los valores se restauren automáticamente.

Resumen de Principios Arquitectónicos

  1. [table] para arrays numéricos O(1)O(1): ideal para tablas de ondas, mapeo de curvas y cuantización directa.
  2. [coll] para bases de datos relacionales simples: perfecto para secuencias de partituras, listas de acordes y eventos en el tiempo.
  3. [dict] es el estándar de oro de la industria: maneja árboles JSON anidados legibles, exportables y universales.
  4. Paso por referencia de diccionarios: los objetos dict comparten la memoria de sus datos; solo viajan punteros de 8 bytes, protegiendo al procesador de clonaciones lentas.