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.”
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:
┌────────────────────────────────────────────────────────────────────────┐│ JERARQUÍA DE MEMORIA EN MAX │├────────────────────────────────────────────────────────────────────────┤│ 1. MEMORIA DE ÍNDICES DIRECTOS (Numérica / Arrays) [table] ││ • Mapeo estricto Entero Entero. Complejidad O(1). ││ • Tablas de ondas (Wavetables), curvas de velocidad, escalas. ││ ││ 2. MEMORIA ASOCIATIVA Y TABLAS DE SÍMBOLOS [coll] ││ • Mapeo Clave (Int o Símbolo) Lista heterogénea de átomos. ││ • Secuencias polifónicas, eventos con timestamp, bases de datos CSV.││ ││ 3. MEMORIA JERÁRQUICA Y ÁRBOLES JSON [dict] ││ • Árboles anidados (Key-Value Trees), arrays heterogéneos y objetos.││ • Estado global de sintetizadores, configuraciones de sesión, REST. │└────────────────────────────────────────────────────────────────────────┘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 : 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:
1, 60 100 250; // Clave 1: Nota 60, Vel 100, Dur 250ms2, 64 90 500; // Clave 2: Nota 64, Vel 90, Dur 500msintro, 120 4 4; // Clave simbólica "intro": 120 BPM, 4/4
- 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`](https://github.com/Cycling74/max-sdk/blob/main/source/dictionary/dict.route/dict.route.c):
### El Problema de la Clonación de MemoriaSi 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 RegistradoEn C, Max no envía el contenido del diccionario por el cable; **envía únicamente el puntero al símbolo del diccionario registrado**:```cvoid 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; }}Gotchas de la Comunidad Oficial: Nombres Efímeros y Diccionarios Anónimos
En los foros de Cycling ‘74, el error más desconcertante con el que tropiezan los desarrolladores es precisamente ese:
"unable to reference dictionary named u123456789".
¿Por qué ocurre y cómo evitarlo?
- Diccionarios Anónimos vs. Nombrados:
- Si creas un
[dict]sin argumentos, Max le asigna un nombre efímero generado (useguido de un número hash único). - Si ese sub-árbol se crea dinámicamente o se desconecta el cable, el recolector de basura lo destruye y cualquier
[dict.unpack]o[dict.view]aguas abajo arroja el error fatal. - Regla de oro: En arquitectura de producción, asigna siempre un nombre explícito a tus diccionarios maestros (
[dict mi_sintetizador]).
- Si creas un
- Consultas Profundas (
get) vs. Desempaquetado (dict.unpack):- Para consultar una clave anidada muy profunda (ej.
filter::resonance), los desarrolladores experimentados recomiendan usar mensajesget path::to::keydirectos en vez de encadenar múltiples[dict.unpack]. Es más rápido, más limpio y no requiere registrar sub-diccionarios intermedios.
- Para consultar una clave anidada muy profunda (ej.
- Arrays de Diccionarios en JSON:
[dict.unpack]no puede extraer directamente una lista de objetos JSON[ { "id": 1 }, { "id": 2 } ].- Debes iterar con
[dict.iter]o consultar por índice directo: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.
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 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 mensajenextrecorremos la partitura paso a paso, y con un número saltamos a cualquier evento de inmediato.
Escenario 3: 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ónwriteexporta el estado completo a un archivo.jsonen 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 800directamente 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
[table]para arrays numéricos : ideal para tablas de ondas, mapeo de curvas y cuantización directa.[coll]para bases de datos relacionales simples: perfecto para secuencias de partituras, listas de acordes y eventos en el tiempo.[dict]es el estándar de oro de la industria: maneja árboles JSON anidados legibles, exportables y universales.- Paso por referencia de diccionarios: los objetos
dictcomparten la memoria de sus datos; solo viajan punteros de 8 bytes, protegiendo al procesador de clonaciones lentas.