Programación de aplicación multicanal de audio.Otro problema

Elios
#1 por Elios el 02/05/2007
... Para poder exponer mi problema tengo que describir mi programa a grandes rasgos, así que tengo que extenderme un poco, lo siento... Por favor, no pareis de leer, intentad ayudarme... gracias.

Soy estudiante de telecomunicaciones por imagen y sonido (para los que no han leído mi otro hilo), haciendo un proyecto más bien de informática... Una aplicación multicanal de audio capaz de reproducir y registrar audio simultáneamente en C#.

He avanzado mucho ya en el código y en la estructura del programa, pero creo que debería abstraer un poco más mis clases y objetos, ya que empiezo a tener una dependencia entre todos ellos que tiene mi codigo muy "enmarañado".

Os explico brevemente:

- El form principal MDIPrincipal es el que contiene todas las barras de herramientas, los botones de PLAY, STOP... etc, los botones de carga de sesión, gestión de pistas, etc... Una simple interfaz gráfica, no implemento demasiado código en ella.

-En este form existe un objeto de tipo PanelDePistas (control de usuario) , que es el que va a contener un número indefinido de pistas de audio, y además llevará implementadas las funcionalidades de reproducción simultánea de todas las pistas, y grabación de todas las pistas (mediante otras clases internas).

-Luego tenemos una unidad fundamental: El control de usuario UnaPista. A esta pista le podremos asociar un archivo WAV, podremos establecer su Volumen, su Balance, si está en modo MUTE o en modo SOLO...

-Esta pista además contiene un objeto de tipo: GraficaOnda, que como su propio nombre indica es el encargado de leer del archivo WAV y representar gráficamente. Además se encargará de hacer zoom y todo eso. La idea es que sea independiente (dentro de lo posible) del objeto UnaPista.

-Por otro lado tenemos un objeto llamado BaseDeTiempos que simplemente se encarga de representar una base de tiempos, y de establecerle funcionalidades para desplazar la gráfica o posicionar el cursor.

-Y además hay otro objeto llamado Vumetro, que es un control grafico capaz de representar con lucecitas un determinado valor.

.... Habiendo explicado brevemente los objetos principales de mi programa: PanelDePista, UnaPista, GraficaOnda, BaseDeTiempos, Vumetro... Os cuento qué hace que mi código esté tan enmarañado:

El problema principal está en que los objetos deben tener una relación entre sí. Así, por ejemplo, necesitamos acceder desde el objeto GraficaOnda al objeto padre UnaPista para cambiar sus propiedades (por ejemplo, un indicador de nivel de Zoom). Además, por ejemplo, necesitamos acceder desde el objeto UnaPista al objeto padre PanelDePistas, por ejemplo para que éste sepa que se han producido cambios en las propiedades de la pista... etc.

Hasta ahora estaba haciendo todo esto pasando por parámetro el objeto padre en el contructor del objeto. Por ejemplo:

public UnaPista(UserControl PanelPadre)
{
InitializeComponents( );
this.PanelPadre = PanelPadre as PanelDePistas; //Aquí definimos el objeto padre.
}


De esta forma, si desde el objeto GraficaOnda, quería cambiar un dato de PanelDePistas, simplemente llamaba de esta forma:

this.PistaPadre.PanelPadre.PropiedadTal = Cual;

... ¿Problema? Ha llegado un momento en que todos los objetos dependen de todos. Si hago un cambio en PanelDePista necesito cambiar todos los demás, ya que estos acceden a él.
----------------------------------------------
He estado pensando hoy y creo que debería empezar a crear eventos a mis objetos... De esta forma, si cambiamos una propiedad de GraficaOnda, salta un evento y en el objeto padre definimos una rutina para esto. Sin embargo me surgen mis dudas...

Además, me gustaría que los objetos fueran independientes: es decir, si modifico PanelDePista, que UnaPista no se entere, y viceversa. Y además me gustaría "ensamblar" todas las clases y subcontroles que forman un control en un único Control de Usuario para poder usarlo de forma independiente incluso en otro proyecto. ¿Cómo se hace esto?.

------- Resumen de preguntas:
1- ¿Cómo se gestionan los componentes en programas complejos cuando estos deben tener relación entre sí?
¿Es correcto pasar el objeto padre por parámetro, o esto a la larga es una chapuza?


2.- ¿Cómo puedo asociar distintas clases a un mismo Control De Usuario, para compilarlo de forma independiente y usarlo en distintos proyectos?
¿Deben las clases internas aparecer como "internal" siempre?
¿Qué sucede si hay clases que quiero usar en varios controles a la vez, las defino como "internal" varias veces en los distintos controles?
¿No rompe esto la norma de reutilización de código?


------
Muchísimas gracias de antemano, y lo siento por haberme extendido tanto pero no he sabido resumir más.
Un saludo
Emilio.
Subir
resonic
#2 por resonic el 02/05/2007
Un par de consejos a mi entender ...

Elios @ 02 May 2007 - 02:48 AM escribió:
... Para poder exponer mi problema tengo que describir mi programa a grandes rasgos, así que tengo que extenderme un poco, lo siento... Por favor, no pareis de leer, intentad ayudarme... gracias.

Soy estudiante de telecomunicaciones por imagen y sonido (para los que no han leído mi otro hilo), haciendo un proyecto más bien de informática... Una aplicación multicanal de audio capaz de reproducir y registrar audio simultáneamente en C#.

He avanzado mucho ya en el código y en la estructura del programa, pero creo que debería abstraer un poco más mis clases y objetos, ya que empiezo a tener una dependencia entre todos ellos que tiene mi codigo muy "enmarañado".

Os explico brevemente:

- El form principal MDIPrincipal es el que contiene todas las barras de herramientas, los botones de PLAY, STOP... etc, los botones de carga de sesión, gestión de pistas, etc... Una simple interfaz gráfica, no implemento demasiado código en ella.
-En este form existe un objeto de tipo PanelDePistas (control de usuario) , que es el que va a contener un número indefinido de pistas de audio, y además llevará implementadas las funcionalidades de reproducción simultánea de todas las pistas, y grabación de todas las pistas (mediante otras clases internas).
-Luego tenemos una unidad fundamental: El control de usuario UnaPista. A esta pista le podremos asociar un archivo WAV, podremos establecer su Volumen, su Balance, si está en modo MUTE o en modo SOLO...

Acostumbrate a utilizar nombres de objetos, clases, propiedades, etc mas aclaratorios. Así si otra persona tiene que leer tu código o vuelves a utilizarlo al cabo de una larga temporada, de un vistazo va a saber de qué estás hablando.

FrmPrincipal : es el formulario principal MDIPrincipal
UCPanelDePistas : es el control de usuario PanelDePistas
UCPista : el control de usuario de la pista. Eso de "UnaPista"? es que luego hay otro que se llama "DosPistas"?

Te podrá parecer una chorrada pero créeme, es muy útil. Pero te preguntarás para que narices necesito hacer eso si con el visualstudio me pongo encima de una propiedad y me sale qué tipo de objecto es?. Imagínate que mañana necesitas hacer una versión para mac (en mono) de tu aplicación o que los señores de Native Instruments te compran tu proyecto.

-Esta pista además contiene un objeto de tipo: GraficaOnda, que como su propio nombre indica es el encargado de leer del archivo WAV y representar gráficamente. Además se encargará de hacer zoom y todo eso. La idea es que sea independiente (dentro de lo posible) del objeto UnaPista.


Create dentro de la solución un nuevo proyecto donde puedas instanciar y probar tus controles de usuario, clases, etc. para comprobar realmente si son independientes.
Independientemente de esto, deberías valorar el "nivel de independencia" que quieres que tengan tus clases del proyecto. No vaya a ser que al final te tires más tiempo en "independizar" que en el propio proyecto.


-Por otro lado tenemos un objeto llamado BaseDeTiempos que simplemente se encarga de representar una base de tiempos, y de establecerle funcionalidades para desplazar la gráfica o posicionar el cursor.

-Y además hay otro objeto llamado Vumetro, que es un control grafico capaz de representar con lucecitas un determinado valor.

.... Habiendo explicado brevemente los objetos principales de mi programa: PanelDePista, UnaPista, GraficaOnda, BaseDeTiempos, Vumetro... Os cuento qué hace que mi código esté tan enmarañado:

El problema principal está en que los objetos deben tener una relación entre sí. Así, por ejemplo, necesitamos acceder desde el objeto GraficaOnda al objeto padre UnaPista para cambiar sus propiedades (por ejemplo, un indicador de nivel de Zoom). Además, por ejemplo, necesitamos acceder desde el objeto UnaPista al objeto padre PanelDePistas, por ejemplo para que éste sepa que se han producido cambios en las propiedades de la pista... etc.

Hasta ahora estaba haciendo todo esto pasando por parámetro el objeto padre en el contructor del objeto. Por ejemplo:

public UnaPista(UserControl PanelPadre)
{
InitializeComponents( );
this.PanelPadre = PanelPadre as PanelDePistas; //Aquí definimos el objeto padre.
}


De esta forma, si desde el objeto GraficaOnda, quería cambiar un dato de PanelDePistas, simplemente llamaba de esta forma:

this.PistaPadre.PanelPadre.PropiedadTal = Cual;

... ¿Problema? Ha llegado un momento en que todos los objetos dependen de todos. Si hago un cambio en PanelDePista necesito cambiar todos los demás, ya que estos acceden a él.


Exacto. De este modo hay dependencia. Una pista siempre depende de PanelDePistas. Aunque en tu código, en el constructor le pasas un control de usuario y no un PanelDePistas. ¿Realmente necesitas esa independencia?. Ten cuidado con el constructor que has hecho. Ya sabes que si PanelPadre no es un PanelDePistas, el valor de this.PanelPadres en null.

Aun así yo no haría el truquito del constructor. Yo me crearía en la clase PanelDePistas una colección de pistas "listaPistas" de este modo (ejemplo):

List ListaDePistas = new List(); // Mírate las colecciones genéricas que son muy útiles y optimizadas (System.Collections.Generic)

Y como bien dices, create los eventos y sus correspondientes delegados para la gestión de las comunicaciones entre objectos.



----------------------------------------------
He estado pensando hoy y creo que debería empezar a crear eventos a mis objetos...


Sí, es lo más limpio.


De esta forma, si cambiamos una propiedad de GraficaOnda, salta un evento y en el objeto padre definimos una rutina para esto. Sin embargo me surgen mis dudas...

Además, me gustaría que los objetos fueran independientes: es decir, si modifico PanelDePista, que UnaPista no se entere, y viceversa.


Con los eventos te aseguras la independecia.


Y además me gustaría "ensamblar" todas las clases y subcontroles que forman un control en un único Control de Usuario para poder usarlo de forma independiente incluso en otro proyecto. ¿Cómo se hace esto?.

------- Resumen de preguntas:
1- ¿Cómo se gestionan los componentes en programas complejos cuando estos deben tener relación entre sí?
¿Es correcto pasar el objeto padre por parámetro, o esto a la larga es una chapuza?



Yo te recomiendo que no lo hagas.


2.- ¿Cómo puedo asociar distintas clases a un mismo Control De Usuario, para compilarlo de forma independiente y usarlo en distintos proyectos?
¿Deben las clases internas aparecer como "internal" siempre?
¿Qué sucede si hay clases que quiero usar en varios controles a la vez, las defino como "internal" varias veces en los distintos controles?
¿No rompe esto la norma de reutilización de código?



Yo no utilizaría clases internas. ¿Para qué?. Así obligas a que sólo se puedan usar en el mismo ensamblado.
Lo de antes: Create un proyecto nuevo de test (o incluso una nueva solución con otro ensamblado) , agregale la referencia de tu proyecto actual y a probar. Te darás cuenta en seguida de los "flecos de independencia" que tiene tu aplicación.


------
Muchísimas gracias de antemano, y lo siento por haberme extendido tanto pero no he sabido resumir más.
Un saludo
Emilio.



Ánimo con el proyecto :)
Subir
Elios
#3 por Elios el 02/05/2007
Muchas gracias por la respuesta, me has sido de gran ayuda.

Lo de pasar un UserControl por parámetro es porque me daba un error si pasaba un PanelDePistas... Así averigué que tenía que pasar un UserControl y luego pasarlo a la clase derivada.

No creo que me lleve demasiado tiempo (un par de dias) cambiar todo mi código y usarlo con eventos. Estoy haciendo una lista de propiedades, métodos y eventos que debe tener cada objeto. De esta forma, hará lo mismo que hace ahora mismo pero con eventos.

...
Es una pena que no me vaya a dar tiempo a mejorarlo mucho... estaba pensando incluso en incluirle compatibilidad ASIO y capacidad de gestionar Plugins VST (o igual lo estaba flipando yo, jeje).

Por ahora uso los drivers WDM para audio (los normalicos, de winmm.dll) y para gráficos uso GDI (un poco lenta la verdad, pero muy sencilla de usar).

Además, he conseguido usar rutinas de Matlab desde C#, algo muy práctico a la hora de desarrollar filtros y efectos... No se si me dará tiempo.

En fin, cuando lo tenga acabado y presentado ya subiré por aquí todo, la memoria y el programa finalizado. Igual sirve de ayuda para gente que quiera hacerse sus propias aplicaciones de audio.
Subir
Futhark
#4 por Futhark el 02/05/2007
te será muy util lo de usar rutinas Matlab, y ya no digamos gestionar VST.. aunque esto ultimo lo veo mas chungo, tienes alguna documentación al respecto? Animo ;)
Subir
Elios
#5 por Elios el 02/05/2007
Por cierto, un par de cosas más...

¿Me recomiendas que haga ensamblados yo? ¿¿Para qué sirve hacerse uno su propio ensamblado??

Yo entiendo por ensamblado un proyecto, que genera un EXE o una DLL (biblioteca de clases), pero no se qué utilidades más tiene...

---

Otra cosa.
Me gustaría comentar a modo informativo solamente la forma en la que me las he apañado para representar las gráficas, ya que en su momento no encontré demasiada información, y me parece que puede ayudar a gente.

Teóricamente, para representar una gráfica tenemos que ir leyendo cada muestra e ir dibujándola. El problema está en que los archivos WAV suelen ser muy grandes (decenas de megas), y es demasiado lento leer del fichero WAV todas las muestras cada vez que hagamos un zoom o desplazemos la gráfica. Además también existe un problema con guardar los ficheros en arrays o listas enlazadas, ya que es demasiada memoria RAM la necesaria (imaginemos 8 archivos de 50 megas: 400megas de RAM usados).

Asi que hay que buscar métodos ingeniosos que no consuman demasiada memoria RAM y además requieran un mínimo acceso a disco. Yo he conseguido esto mediante el uso de "archivos de picos". Voy a explicar por qué son útiles.

Imaginemos un archivo WAV a 44100HZ de 5 minutos, que queremos representarlo entero en una gráfica de 600 pixels de largo. Son 13230000 muestras que debemos encajar en 600 pixels. Por tanto, en cada linea de grosor 1 pixel debemos encajar 22050 muestras. Si vemos qué resulta de pintar 22050 puntos con la misma coordenada X todo el rato, veremos que es una línea vertical, cuyo punto inferior es la muestra más pequeña de las 22050, y cuyo punto superior es la muestra mayor de las 20050.

Pensando un poco se llega a la conclusión, de que para representar este fichero WAV completo en la gráfica de 600 pixel sólo necesitamos 1200 datos: La muestra inferior y la muestra superior correspondiente a cada coordenada X, para despues trazar una línea vertical.

Pues bien, lo que yo he hecho es guardar en un fichero (.pik lo he llamado, aunque en Adobe Audition lo llama .pk) la muestra mayor y la muestra menor de cada 256 muestras. Si os vais en Audition a Options --> Settings --> Display, vereis un apartado llamado Peak Files en el que podeis seleccionar este valor, que por defecto está en 256.

Así en mi gráfica hay dos casos: Si el número de muestras por pixel es mayor de 256 lee del fichero de picos, si el número de muestras por pixel es menor de 256 (caso zoom cercano), lee directamente del WAV original. En cualquier caso tarda poco, ya que en el caso de leer del fichero WAV original, al haber como máximo 256 muestras por pixel, estaremos cargando para la gráfica de 600 pixel como máximo 153600 muestras (tiempo imperceptible comparado con los 13 millones de muestras de antes).

Así que para representar muestras uso los archivos de picos para zoom alejados, y el archivo WAV para zoom cercanos. Me funciona muy bien y controlo las gráficas como si fueran de programas comerciales.

...
Por otro lado, para los desplazamientos a derecha o a izquierda, simplemente copio y pego el buffer de imagen representado un poco desplazado, y cargo del fichero sólamente las muestras nuevas que hay que representar (la del lado derecho si desplazamos a la izquierda, y viceversa). Esto hace mucho menos pesado el algoritmo de desplazamiento.

Espero ayudarle a alguien!

P.D: En la memoria casi que copiaré y pegaré esta explicación, jeje.
Subir
Elios
#6 por Elios el 02/05/2007
Lo de VST no tengo demasiada documentación, aunque Audacity (software libre en C++) soporta plugins VST y su código está distribuido.

He visto como crear plugins VST, que parece no ser demasiado complicado, pero usarlos no se cómo habría que hacerlo. Supongo que en C# debe existir algún ejemplo por ahí, pero hay que encontrarlo.

Bye!
Subir
resonic
#7 por resonic el 02/05/2007
¡Muy interesante!

Por cierto, has barajado la idea de trabajar con directx? Hace unos años hice pruebas en 5.1 y está muy bien. Realmente muy cómodo. En The code project tienes muchos ejemplos. Si no conoces esta web, échale un vistazo que hay auténticas genialidades. Aquí tienes un ejemplo simple usando directsound:

http://www.codeproject.com/cs/media/cswavrec.asp

En msdn encontrarás prácticamente todo sobre .net C#. Aquí tienes una explicación sobre los ensamblados de .net:
http://msdn2.microsoft.com/es-es/library/ms173099(VS.80).aspx


Elios @ 02 May 2007 - 02:56 PM escribió:
Por cierto, un par de cosas más...

¿Me recomiendas que haga ensamblados yo? ¿¿Para qué sirve hacerse uno su propio ensamblado??

Yo entiendo por ensamblado un proyecto, que genera un EXE o una DLL (biblioteca de clases), pero no se qué utilidades más tiene...

---

Otra cosa.
Me gustaría comentar a modo informativo solamente la forma en la que me las he apañado para representar las gráficas, ya que en su momento no encontré demasiada información, y me parece que puede ayudar a gente.

Teóricamente, para representar una gráfica tenemos que ir leyendo cada muestra e ir dibujándola. El problema está en que los archivos WAV suelen ser muy grandes (decenas de megas), y es demasiado lento leer del fichero WAV todas las muestras cada vez que hagamos un zoom o desplazemos la gráfica. Además también existe un problema con guardar los ficheros en arrays o listas enlazadas, ya que es demasiada memoria RAM la necesaria (imaginemos 8 archivos de 50 megas: 400megas de RAM usados).

Asi que hay que buscar métodos ingeniosos que no consuman demasiada memoria RAM y además requieran un mínimo acceso a disco. Yo he conseguido esto mediante el uso de "archivos de picos". Voy a explicar por qué son útiles.

Imaginemos un archivo WAV a 44100HZ de 5 minutos, que queremos representarlo entero en una gráfica de 600 pixels de largo. Son 13230000 muestras que debemos encajar en 600 pixels. Por tanto, en cada linea de grosor 1 pixel debemos encajar 22050 muestras. Si vemos qué resulta de pintar 22050 puntos con la misma coordenada X todo el rato, veremos que es una línea vertical, cuyo punto inferior es la muestra más pequeña de las 22050, y cuyo punto superior es la muestra mayor de las 20050.

Pensando un poco se llega a la conclusión, de que para representar este fichero WAV completo en la gráfica de 600 pixel sólo necesitamos 1200 datos: La muestra inferior y la muestra superior correspondiente a cada coordenada X, para despues trazar una línea vertical.

Pues bien, lo que yo he hecho es guardar en un fichero (.pik lo he llamado, aunque en Adobe Audition lo llama .pk) la muestra mayor y la muestra menor de cada 256 muestras. Si os vais en Audition a Options --> Settings --> Display, vereis un apartado llamado Peak Files en el que podeis seleccionar este valor, que por defecto está en 256.

Así en mi gráfica hay dos casos: Si el número de muestras por pixel es mayor de 256 lee del fichero de picos, si el número de muestras por pixel es menor de 256 (caso zoom cercano), lee directamente del WAV original. En cualquier caso tarda poco, ya que en el caso de leer del fichero WAV original, al haber como máximo 256 muestras por pixel, estaremos cargando para la gráfica de 600 pixel como máximo 153600 muestras (tiempo imperceptible comparado con los 13 millones de muestras de antes).

Así que para representar muestras uso los archivos de picos para zoom alejados, y el archivo WAV para zoom cercanos. Me funciona muy bien y controlo las gráficas como si fueran de programas comerciales.

...
Por otro lado, para los desplazamientos a derecha o a izquierda, simplemente copio y pego el buffer de imagen representado un poco desplazado, y cargo del fichero sólamente las muestras nuevas que hay que representar (la del lado derecho si desplazamos a la izquierda, y viceversa). Esto hace mucho menos pesado el algoritmo de desplazamiento.

Espero ayudarle a alguien!

P.D: En la memoria casi que copiaré y pegaré esta explicación, jeje.
Subir
resonic
#8 por resonic el 02/05/2007
En c# hay poca cosa. Mírate esto: tiene muy buena pinta.

http://confluence.public.thoughtworks.o ... Noise/Home

Elios @ 02 May 2007 - 03:04 PM escribió:
Lo de VST no tengo demasiada documentación, aunque Audacity (software libre en C++) soporta plugins VST y su código está distribuido.

He visto como crear plugins VST, que parece no ser demasiado complicado, pero usarlos no se cómo habría que hacerlo. Supongo que en C# debe existir algún ejemplo por ahí, pero hay que encontrarlo.

Bye!
Subir
Respuesta rápida

Regístrate o para poder postear en este hilo