Objetos y conceptos de control

En la interacción con un computador se hace necesaria la toma de decisiones, es decir, programar alguna forma de reacción a la información entrante. El primer caso de este tipo de control es llamado condicional y es en general un momento en el cual se evalúa la información entrante para determinar si se cumple alguna condición para hacer algo y si no se cumple se hace otra cosa.

Una posibilidad para los condicionales es anidarlos, esto es, condicionales dentro de condicionales, lo cual es útil para determinar casos, por ejemplo si estamos determinando reacciones a alturas en un flujo melódico para comenzar procesos de modificación de la señal. Otra posibilidad es que en el flujo de información de un aparato con el que se envía información al computador (como un sensor, una tableta, otro computador, un dispositivo midi, etc.), se necesite identificar de cual dispositivo o parte del mismo viene la información y hacia dónde se envía para cumplir con alguna tarea asignada.

Todas estas decisiones pueden ser pensadas como árboles, en los cuales el punto de partida es la información y a medida que se avanza en las ramas se obtiene la identificación de la situación específica que se tiene. Observemos el gráfico a continuación para entender mejor esta situación.


El anterior es un árbol con el cual podemos identificar las situaciones de condicionales previamente descritas. En este caso, el algoritmo nos permite determinar el orden de una combinación de las letras A, B y C. En el primer paso se pregunta cuál es la primera letra, lo cual a su vez puede ser un árbol y no ha sido incluido en este gráfico. Una vez determinada la primera letra se pregunta cuál es la segunda y se pueden obtener rápidamente las dos respuestas, por ejemplo, si la primera letra ha sido identificada como A y preguntamos ¿La segunda letra es B?  tenemos dos respuestas posibles: B o C, si la respuesta es "Si, es B" vamos a la izquierda de este árbol y se habrá identificado el orden ABC, pero si la respuesta es "No" entonces vamos a la derecha y el orden es ACB.

* Como ejercicio para entender el funcionamiento de este ejemplo pruebe con las cadenas CAB, BCA y CBA.

En Pure Data, hay varios objetos que hacen uso de condicionales para cumplir con tareas o incluso hay objetos que son simplemente casos de condicionales. A continuación estudiaremos algunos de estos en el contexto del uso de listas de información como flujo entrante.

Supongamos que se han conectado varios computadores a una red local y están intercambiando mensajes para controlar diversos procesos en los demás computadores, por ejemplo, retrasos en la señal, la envolvente del sonido o el control de un sintetizador simple de modulación de frecuencia. En cada computador cada mensaje es tratado de una manera diferente, para que un usuario controle un proceso distinto en cada computador.

En los ejemplos a continuación, paralelamente a la explicación del funcionamiento de los objetos, vamos a aplicar lo explicado hasta ahora de condicionales.

EJEMPLO 1.

Supongamos que tenemos tres usuarios: Mateo, Uriel y Sebastián. Lo primero que queremos hacer es identificar de cual computador viene la información que se está procesando. En este caso, también vamos a asumir que se está usando como método para codificar la información Open Sound Control (OSC). Para estos ejemplos, es necesaria la inclusión de la librería mrpeach, basta con poner un objeto |import mrpeach|. 

El objeto |routeOSC| permite identificar listas de información que comiencen con el identificador "/ " y que seguido tengan algún nombre específico, luego de remover esa parte de la información envía a la salida correspondiente a ese identificador el resto de la información en la lista. En el ejemplo, podemos observar que los nombres de los usuarios son la primera pregunta que se realiza, los identificadores son "/mateo", "/uriel" y "/sebastian". En ese orden se crean las salidas de izquierda a derecha, pero como podemos observar hay una cuarta salida en ese objeto, la cual va a corresponder a cualquier otra información que no concuerde con los identificadores que se han escrito como argumentos del objeto.

En el ejemplo, vemos que se ha conectado desde la segunda salida, la correspondiente al identificador "/uriel", otro objeto con selección de casos haciendo uso de |routeOSC|, en el cual se identificará otra parte más de la información que debe ser llevada hacia algún proceso en particular. Estos procesos de selección son como el ejemplo de árbol que vimos previamente. Vamos a suponer que entra el siguiente mensaje y vamos a ver cómo se procesa en estas conexiones de objetos:


Como estudiamos previamente, al entrar la lista de información /uriel/envolvente 0 1 3000, el objeto |routeOSC| envía la información a la segunda salida y retira la esa parte de la información de la lista de información. Después al entrar al segundo objeto |routeOSC| esa información se envía a la salida que le corresponde al identificador "/envolvente" retirando de la lista de información el identificador y dejando el resto de la lista. 

En el último paso también es posible usar el objeto |route|, el cual tiene un funcionamiento similar, pero que en lugar de entender como identificador un nombre que comienza con el carácter "/" sino con un espacio. Suponiendo que en la última pregunta no existiera otra posibilidad de identificación con OSC es posible usar |route|, pues tomará como palabra identificadora /envolvente. Si el caso fuese que otro identificador de OSC se hiciera presente más adelante, entonces es totalmente necesario el uso de |routeOSC|, pues |route| no entenderá como dos palabras distintas la separación con el carácter "/", sino que las identificará como una sola palabra por la ausencia de espacio, por ejemplo si llega el mensaje "/envolvente/set 1" se enviaría en el objeto |route| a la última salida, pues no existiría coincidencia.

EJEMPLO 2


En este nuevo ejemplo, se implementa como penúltimo paso el uso del objeto |route| del cual se habló en el ejemplo anterior. Su uso es similar al de |routeOSC|, la diferencia radica en que los mensajes pueden omitir como identificador el carácter "/" y hacer uso de la separación con espacios. En este caso, se asume que el último nivel de identificación o de selección de casos no tienen más posibilidades de uso del signo "/" y entonces no es necesario el uso de |routeOSC|.

Adicionalmente en este ejemplo, hay un objeto nuevo, se trata de |unpack|. Este objeto sirve para convertir una lista de información en elementos individuales de otro tipo de dato. En el ejemplo podemos observar que se ha escrito como argumentos en el objeto tres efes separadas por espacios, las cuales significan que se separará la lista en tres elementos del tipo de dato float (abreviado f). Si se diera el caso en el cual la lista de información que llega a este |unpack| no estuviera conformada por números del tipo de dato esperado, el objeto arrojará un error en la consola diciendo que esperaba un tipo de dato y le llegó otro.

El objeto |unpack| recibe como argumentos las letras f, s y p, que significan respectivamente float, symbol y pointer. Si se desea profundizar en cada uno de estos ponga un objeto con esos nombres y pulse clic derecho para consultar la ayuda.

* Como ejercicio para este ejemplo, reconstruya el mensaje que ingresó.  

EJEMPLO 3


Otra posibilidad de manejo para la información es que no necesariamente se le tenga un momento de identificación como hemos hecho en los dos ejemplos anteriores, sino que ya conociendo de antemano en cuales puntos debe ser dividida la lista se use como información condicional la posición del último elemento en la sub-lista o informaciones más pequeñas en las que se debe dividir la lista de información inicial. 

En esta ocasión usaremos el objeto |zl| el cual tiene diferentes tipos de operaciones posibles para las listas. Vamos a usar la operación slice (dividir) que toma como argumento una posición en la lista que entra y la divide en dos listas o dos informaciones diferentes. Saca por la izquierda del objeto desde el primer elemento hasta el elemento que se le indique como posición y por la derecha desde el elemento después del indicado hasta el final de la lista. 

Todo el proceso del cual se ve la información final dividida en el ejemplo de arriba, se puede ver paso por paso en el siguiente gráfico:


Es necesario observar que la determinación de los puntos en los cuales debe ser dividida la lista responden a la lógica de determinar desde el último paso hasta el primero. En esta ocasión, el último paso ha sido dividir una lista de dos elementos en elementos individuales, para lo cual se ha tomado el primer elemento de esa lista como argumento de la operación. Si vamos un paso más atrás, tenemos una lista de cuatro elementos, que debe ser dividida en dos listas de dos elementos cada una. Otro paso más atrás nos lleva a una lista de siete elementos, en la cual se divide desde el tercer elemento para obtener listas de tres elementos y cuatro elementos respectivamente (de izquierda a derecha). 

Siempre es posible hacer uso de |unpack| para cualquiera de las listas que sean creadas. Por ejemplo, el último paso pudo haber sido un |unpack f f |, sin embargo para el propósito de este ejemplo no fue considerado. La decisión de usar uno u otro debe obedecer a qué se va a hacer con la información más adelante, si va a ser usada como lista o si va a ser usada como información individualizada. Exploremos un poco más esta opción en el siguiente ejemplo.

EJEMPLO 4

En algunas ocasiones, una sola fuente de información se puede convertir en un objeto de interés para dividir en diferentes direcciones, ya sea para procesar la información que se obtiene de esas divisiones o para disparar otros procesos. En dichos casos también es posible usar condicionales para dividir la información. Para el siguiente caso vamos a usar la pregunta ¿Es mayor o igual? 

Al hacer esa pregunta es posible obtener una respuesta binaria, sí o no. Para el caso que vamos a estudiar, vamos a pensar en esa pregunta asociada a el proceso de enviar la información para un lado o para otro, incluso para enviarlo a diferentes direcciones.

En Pure Data, encontramos un objeto llamado |moses| el cual toma como argumento el número con el cual establecemos esa pregunta, aunque ese número puede ser cambiado a través de la entrada de la derecha. Al ingresar una información numérica por la izquierda se realiza la pregunta y dependiendo del resultado se envía la información hacia una salida. Para el caso de la salida izquierda, se envía la información que es menor al número que se escriba como argumento. La salida derecha es usada para enviar la información que es mayor o igual al número que se designe como argumento.

Como todos los condicionales, es posible hacer preguntas sucesivas a la manera de un árbol de respuestas binarias. Esto resulta particularmente útil para nosotros, pues nos permite establecer esas operaciones que pueden partir desde un solo flujo de información.


En el gráfico podemos observar que el flujo de información se envía a preguntas las sucesivas ¿Es mayor o igual a 30? ¿Es mayor o igual a 51? ¿Es mayor o igual a 79? ¿Es mayor o igual 151? Como se puede observar la salida de la derecha se envía a la siguiente pregunta y así sucesivamente, lo cual permite tener intervalos de información. En este caso la primera caja numérica (izquierda), los números serán todos los menores de 30, la segunda caja numérica sirve para ver los números entre 30 y 50, la tercera para números entre 51 y 78, la cuarta para números entre 79 y 150 y la última, para números mayores o igual a 151.

En algunos casos la información no se transforma en algo más que en un gatillo para que comience algún evento. En tales casos hay varias posibilidades. Para casos en los cuales se define una reacción específica a ciertos datos es posible usar el objeto |select| o su abreviatura |sel|. Este objeto actúa de una manera similar a |route|, pero en lugar de enviar información adicional que tenga el flujo de entrada, lo que se hará es enviar un bang a la salida que coincide con la posición del elemento con el que ocurre la casualidad.

En otros casos, en los que no se requiere tanta especificidad sino que se quiere abarcar todo un rango de información para que tenga una misma reacción entones es mejor usar |moses| y convertir sus salidas numéricas a bang simplemente conectando un bang a la(s) salida(s) que se desee(n) usar de esta manera. 

Hay una reflexión en todo caso que debemos tener para este tipo de uso y es si vamos a requerir que se restrinja el paso de bang hacia donde lo enrutamos después de que ha sido activado por primera vez. Consideremos la siguiente situación: 

  • Hay un flujo de información aleatoria de números entre 0 y 100. Se debe generar una reacción que dura 5 segundos y es disparada por el rango numérico de 0 a 20. Los números entran en intervalos de 10 milisegundos. 


En esta situación es imposible determinar si 10 milisegundos después de una primera activación va a ocurrir otra o si va a permitir que se ejecute el evento de 5 segundos (5000 milisegundos). Lo mejor que se puede hacer es implementar un mecanismo que bloquee el paso de bang hacia el disparador del proceso hasta que se cumpla el tiempo del mismo. En ese caso debemos usar dos objetos |oneshot| y |delay|.  El primero permite el paso de un bang y luego bloquea el paso de bang hasta que le llegue un mensaje que diga "clear" para restablecer el paso. El segundo, |delay| o abreviado |del| permite retrasar una cantidad de tiempo determinado un bang. Juntos estos dos objetos sirven como mecanismo de control para la situación descrita.

* Implemente el siguiente patch y observe los resultados al cambiar la casilla numérica de más arriba. Para visualizar las conexiones de los mensajes conectados a |sel| use el objeto |Print|.


Comentarios

Entradas populares de este blog

Conociendo lo básico de Pure Data

Pure data - Vanilla, lo básico, añadir externos

Síntesis aditiva