Emulación del sonido generado por la ULA

Emuladores y aplicaciones que ayudarán a la perpetuación del Spectrum y su software en el futuro

Moderador: Sir Cilve Sinclair

Emulación del sonido generado por la ULA

Notapor Manu el Vie Feb 22, 2013 12:11 pm

Buenas,

Desde hace unos meses, me he embarcado en un proyecto de "multiemulador" en Java. No tanto porque crea que haga falta uno más, sino porque siempre he querido hacer uno, y Java es el lenguaje en el que suelo programar, y quería aprender algunas técnicas avanzadas.

El caso es que he conseguido que funcionen juegos de Spectrum, Amstrad, Game Boy, Master System y alguna cosilla de NES, aunque nunca me había metido con el tema del sonido. Para empezar, quería ir a lo más sencillo... y he pensado que el sonido de 1 bit que genera la ULA es el mejor candidato 8)

Actualmente, el sonido generado se parece al real (oigo melodías en juegos como Chase HQ, Army Moves,...), pero hay un ruido de fondo enorme. De hecho, sólo con "encender" el Spectrum virtual, suena una especie de zumbido constante bastante molesto.

Voy a explicar mi teoría de cómo creo que funciona para ver si es correcta. Tengo una clase que monitoriza el estado del bit 4 del out de la ULA. Periódicamente, voy sampleando el estado, y si está a 1 genero un pulso PCM alto, y si está a cero, un pulso PCM bajo, para generar una onda cuadrada. Luego eso se lo doy a mi clase reproductora de audio, y el resultado es el que os comentaba antes. ¿Se me escapa algo?

Muchas gracias por leer el tocho :oops:

Un saludo,
Manuel
Avatar de Usuario
Manu
Herbert
 
Mensajes: 89
Registrado: Mie Sep 05, 2007 11:35 pm

Re: Emulación del sonido generado por la ULA

Notapor radastan el Vie Feb 22, 2013 12:37 pm

Primera pregunta, ¿a qué velocidad estás sampleando? porque si tratas de digitalizar una señal digital debes usar el doble de velocidad de sampleado como poco. Sube la lectura del puerto al doble de frecuencia a ver que pasa.
_________________________________________
Hay otras páginas.... pero no son Bytemaniacos
http://www.bytemaniacos.com
Orgullo de 8 bits
_________________________________________
Avatar de Usuario
radastan
Phantomas
 
Mensajes: 2174
Registrado: Lun May 07, 2007 5:34 pm

Re: Emulación del sonido generado por la ULA

Notapor Manu el Vie Feb 22, 2013 12:47 pm

Estoy sampleando a 44100Hz :P
Avatar de Usuario
Manu
Herbert
 
Mensajes: 89
Registrado: Mie Sep 05, 2007 11:35 pm

Re: Emulación del sonido generado por la ULA

Notapor Manu el Vie Feb 22, 2013 12:55 pm

Vale, creo que ya lo he solucionado... el caso es que reservaba un buffer un pelín más grande de lo que necesitaba, y no se llenaba por poco. Ahora sólo devuelvo lo que realmente se llena, y no se oye el ruido raro.

Me he dado cuenta al grabar el sonido con Audacity, y ver que había un pico a 0 cada 20ms :)
Avatar de Usuario
Manu
Herbert
 
Mensajes: 89
Registrado: Mie Sep 05, 2007 11:35 pm

Re: Emulación del sonido generado por la ULA

Notapor radastan el Vie Feb 22, 2013 1:02 pm

Manu escribió:Estoy sampleando a 44100Hz :P


¿En ejecución paralela o sincronizada? es decir, si estás programando en Java monohilo sólo mirarás el puerto cuando realmente le toque al emulador mirar, pero no cuando se genera la señal realmente. Es que no se como tienes realizado el emulador. Yo probaría a crear un pequeño buffer con las 960 muestras que se pueden dar en cada fotograma de imagen, así no pierdes ni un bit.

Es que es muy posible que estés perdiendo bits, eso o que estés emulando mal la ULA (no me cuadra que haga ruido al arrancar).

Echa un vistazo al tutorial que se curraron aquí sobre hacer un emulador de ZX Spectrum (esta es la parte de audio):

http://efepuntomarcos.wordpress.com/2012/09/07/hazte-un-spectrum-7-parte/
_________________________________________
Hay otras páginas.... pero no son Bytemaniacos
http://www.bytemaniacos.com
Orgullo de 8 bits
_________________________________________
Avatar de Usuario
radastan
Phantomas
 
Mensajes: 2174
Registrado: Lun May 07, 2007 5:34 pm

Re: Emulación del sonido generado por la ULA

Notapor Manu el Vie Feb 22, 2013 1:10 pm

El emulador tiene varios hilos, uno de ellos dedicado a la reproducción de sonido 16 bits PCM a 44100Hz. Sí, lo sé, es bastante "bestia", pero a lo mejor algún día emulo algo que necesite calidad CD xD

Las 960 muestras que comentas son si el sonido se reproduce a 48KHz (como en el emulador de FMarcos), en mi caso son 882 :)
Avatar de Usuario
Manu
Herbert
 
Mensajes: 89
Registrado: Mie Sep 05, 2007 11:35 pm

Re: Emulación del sonido generado por la ULA

Notapor radastan el Vie Feb 22, 2013 1:17 pm

Manu escribió:El emulador tiene varios hilos, uno de ellos dedicado a la reproducción de sonido 16 bits PCM a 44100Hz. Sí, lo sé, es bastante "bestia", pero a lo mejor algún día emulo algo que necesite calidad CD xD

Las 960 muestras que comentas son si el sonido se reproduce a 48KHz (como en el emulador de FMarcos), en mi caso son 882 :)


Estupendo, pues ahora tienes que hacer que cada x muestras haga una interpolación y ese valor es el que llevas al PC. Es decir, haz un filtrado de la señal que recoges. Es lo que pasa en los televisores, el sonido no es digital y se escucha de forma analógica (hablamos de los años 80).

A ver que pasa.

También trataría de no recoger la información del puerto fuera de lo que es la representación de imagen (si, que no suene nada durante el retrazo).

PD: Aquí falta Jepalza o McLeod dando su opinión. Creo que Jepalza, con su experiencia haciendo emuladores, te puede guiar mejor.
_________________________________________
Hay otras páginas.... pero no son Bytemaniacos
http://www.bytemaniacos.com
Orgullo de 8 bits
_________________________________________
Avatar de Usuario
radastan
Phantomas
 
Mensajes: 2174
Registrado: Lun May 07, 2007 5:34 pm

Re: Emulación del sonido generado por la ULA

Notapor zx81 el Vie Feb 22, 2013 1:49 pm

Puedes coger los fuentes de mi emulador y echarles un vistazo. Está todo en la clase Audio.java.

El sonido es complicado de emular porque lo necesitas en "tiempo real" para que no se oigan cosas raras, es fundamental que el buffer no se vacíe nunca cosa, dicho sea de paso, casi imposible de conseguir al 100%, así que debes bregar con la situación de que el buffer de audio se vacíe, se escuche algo raro (inevitable), y eso no afecte a la emulación subsiguiente (o sea, que no se quede el sonido estropiciao hasta que pares el emulador).

Por otro lado, "samplear" el bit del puerto #FE es innecesario. Para cambiar ese bit el programa debe ejecutar un out al puerto, así que lo más fácil es llevar un contador de t-estados general, y en cada out calcular el número exacto de t-estados que ha durado el bit con ese valor usando esa diferencia para calcular cuantos samples tienes que generar. Y, como el diablo está en los detalles, te diré que la fidelidad de la emulación del beeper está en los decimales.... ;)

Otra cosa que te puede dar problemas a la larga es asignar valores a los samples del tipo 0 = -7000 y 1 = 7000 porque eso lleva la señal a nivel -7000 en el caso habitual, lo que dificulta (mucho) la mezcla de otras fuentes de sonido que haga el S.O. que uses, además de generar "cliks" cada vez que arranques y pares el emulador. Mientras te limites a emular el beeper quizá no encuentres muchos problemas, pero si añades emulación del AY, seguro que ese asunto te da más de un dolor de cabeza.
Debido al fallo de un mecanismo, el lanzagranadas M203 se te podía disparar cuando menos lo esperaras, lo que te habría hecho bastante impopular entre lo que quedara de tu unidad.
Revista del ejército EE.UU. PS, agosto 1993.

Emulador JSpeccy
ZXBaremulator
zx81
Freddy Hardest
 
Mensajes: 591
Registrado: Vie Dic 28, 2007 3:14 pm
Ubicación: Valencia

Re: Emulación del sonido generado por la ULA

Notapor Manu el Vie Feb 22, 2013 3:48 pm

Gracias por la explicación. Echaré un ojo a tus fuentes ;)

Realmente lo de "samplear" no refleja lo que hago. Tengo una clase asociada a la ULA, que cada vez que se hace un out, guarda el valor. Esa clase se la actualizo con la CPU, y periódicamente voy generando los samples. Lo malo es que he visto que a veces se generan 880 y otras 881, cuando deberían ser 882... por eso oigo el sonido entrecortado a veces :P

La verdad es que el tema del sonido es lo más complicado de hacer un emulador, porque como dices todo tiene que ir en tiempo real...
Avatar de Usuario
Manu
Herbert
 
Mensajes: 89
Registrado: Mie Sep 05, 2007 11:35 pm

Re: Emulación del sonido generado por la ULA

Notapor zx81 el Vie Feb 22, 2013 5:42 pm

El problema es que tú ves los samples en tiempo "real", pero los deberías ver en tiempo de CPU. Suponiendo que estés emulando el 48k, que funciona a 3,5 Mhz:

3.500.000 / 44100 = 79,36507 t-states por sample.

Ni se te ocurra redondear ese número, ni siquiera a 79,3 o 79,4 o ya estarías en problemas.

Por otro lado, cada frame dura 69888 t-states. Pero eso es solo a nivel de video estrictamente hablando, porque la CPU reconoce la interrupción al final de cada instrucción y eso, en el peor caso (no es el "más peor", pero es que no quiero entrar en casos degenerados que solo pueden darse haciéndolo adrede), te puede llevar a 69887 + 23 t-states que dura la instrucción más larga del Z80 si la memoria no me falla. Puede que en ese frame generes hasta 883 samples, que en el siguiente serán reducidos a 881 probablemente porque tendrás un frame más corto, por decirlo de alguna manera.

En cualquier caso, los frames tienden a estar desalineados con los samples y esa desalineación se va desplazando en función de cuanto dure realmente cada frame. O sea, que no puedes asumir que cada frame empieza con un sample nuevo, ni puedes deshacerte de lo que te sobra al final de cada frame, tienes que acumularlo obligatoriamente y guardarlo para el siguiente.

No vas a poder generar exactamente los 882 samples por frame, eso es seguro. Además, sin saber cómo has montado el tema del reloj, tienes que contar con que no siempre vas a tener la CPU a tu disposición de forma inmediata cuando la necesites, así que tienes que curarte en salud y meter al inicio hasta un frame adelantado de datos, aunque sean ceros, para dar un margen al scheduler del sistema operativo y a tu propia aplicación. Y ojito, porque el scheduler de Linux no se comporta igual que el de Solaris, y en nada se parece al ultra-caótico scheduler de Windows que depende hasta de la fecha de la próxima menstruación probable de la reina Cleopatra. Como no sé en qué sistema estás desarrollando, prefiero avisarte para que no te lleves sorpresas.

Tampoco puedes meter más de un frame de audio adelantado o puedes ver como, en juegos tipo Arkanoid, la bola golpea el ladrillo y el sonido lo escuchas cuando la bola ya está a mitad de la pantalla.

Y dependiendo de la carga del sistema, directamente puede ser que no haya forma de que suenen bien más de 3 o 4 frames seguidos pero eso ya.... tiene mala solución. Incluso en esa situación deberías ser capaz de "defenderte" y que suene bien si la carga del sistema desciende y no se te quede sonando el emulador como una cerda pariendo botellas rotas.

Te huro por mis ninios que las escasas 30 líneas que emulan al beeper en JSpeccy son las líneas que más trabajo me ha costado llegar a escribir en toda mi vida. Fueron semanas de pruebas.....
Debido al fallo de un mecanismo, el lanzagranadas M203 se te podía disparar cuando menos lo esperaras, lo que te habría hecho bastante impopular entre lo que quedara de tu unidad.
Revista del ejército EE.UU. PS, agosto 1993.

Emulador JSpeccy
ZXBaremulator
zx81
Freddy Hardest
 
Mensajes: 591
Registrado: Vie Dic 28, 2007 3:14 pm
Ubicación: Valencia

Re: Emulación del sonido generado por la ULA

Notapor mcleod_ideafix el Vie Feb 22, 2013 8:06 pm

radastan escribió:PD: Aquí falta Jepalza o McLeod dando su opinión. Creo que Jepalza, con su experiencia haciendo emuladores, te puede guiar mejor.

Huy, el Java y yo, cuanto más lejos uno de otro, mejor :D
Web: ZX Projects | Twitter: @zxprojects
Avatar de Usuario
mcleod_ideafix
Johnny Jones
 
Mensajes: 3981
Registrado: Vie Sep 21, 2007 1:26 am
Ubicación: Jerez de la Frontera

Re: Emulación del sonido generado por la ULA

Notapor radastan el Vie Feb 22, 2013 8:50 pm

mcleod_ideafix escribió:
radastan escribió:PD: Aquí falta Jepalza o McLeod dando su opinión. Creo que Jepalza, con su experiencia haciendo emuladores, te puede guiar mejor.

Huy, el Java y yo, cuanto más lejos uno de otro, mejor :D


Lo decía más por saber que provoca el ruido de fondo y el del arranque.
_________________________________________
Hay otras páginas.... pero no son Bytemaniacos
http://www.bytemaniacos.com
Orgullo de 8 bits
_________________________________________
Avatar de Usuario
radastan
Phantomas
 
Mensajes: 2174
Registrado: Lun May 07, 2007 5:34 pm

Re: Emulación del sonido generado por la ULA

Notapor Manu el Vie Feb 22, 2013 10:55 pm

radastan, el problema del zumbido ya está arreglado. Como ya he dicho en un mensaje anterior, era un tema del buffer. Había valores a cero al final, y provocaban el zumbido a 50Hz :)

Por otro lado zx81, lo que indicas acerca de los timings más o menos lo tengo en cuenta. Lo que sí es cierto es que a pesar de que la emulación del Z80 es bastante completa (con los flags ocultos, el MEMPTR, y todas las operaciones no documentadas funcionando), la del Spectrum como máquina es muy básica (no hay contienda, y el frame lo construyo al final en vez de instrucción a instrucción). De hecho, es una arquitectura que monté para poder ver los tests del Z80, que casi todos eran para Spectrum 8)

Para que te hagas una idea, la clase que emula el sonido de 1 bit es esta:

Código: Seleccionar todo
package com.emubee.sound;

public class SingleBitAudio {
   protected double microseconds;
   protected final static double MICROSECONDS_PER_CYCLE = 1000000.0 / 44100.0;
   protected boolean state;
   protected byte[] buffer;
   protected int pos;
   
   public SingleBitAudio(int samples) {
      buffer = new byte[samples * 4];   // 16 bits, stereo
      pos = 0;
   }
   
   public void reset() {
      pos = 0;
   }
   
   public void update(double time) {
      microseconds += time;
      if (microseconds >= 0) {
         buffer[pos++] = (byte) (state ? 32 : -32);
         buffer[pos++] = 0;
         buffer[pos++] = (byte) (state ? 32 : -32);
         buffer[pos++] = 0;
         microseconds -= MICROSECONDS_PER_CYCLE;
      }
   }
   
   public void setState(boolean state) {
      this.state = state;
   }
   
   public byte[] getBuffer() {
      byte[] res = new byte[pos];
      System.arraycopy(buffer, 0, res, 0, pos);
      return res;
   }
}


Y el método que "renderiza" un frame en el Spectrum es este:

Código: Seleccionar todo
public void renderFrame() {
      boolean ready = false;
      audio.reset();
      while (!ready) {
         int c = cpu.execute();
         ready = ula.update(c);
         double microseconds = c * MICROSECONDS_PER_CYCLE;
         cas.update(microseconds);
         audio.update(microseconds);
      }
      frameConverted = false;
   }


Tanto el gestor de cassete como el del audio trabajan en microsegundos y no en ciclos del Spectrum, porque los uso en otras máquinas. De hecho, la carga de cintas TZX se hace exactamente igual en el Spectrum que en el Amstrad CPC... y funciona :)

En cualquier caso, me temo que voy a tener que pelearme bastante con el sonido, ya que nunca había programado nada "multimedia" en mi vida...
Avatar de Usuario
Manu
Herbert
 
Mensajes: 89
Registrado: Mie Sep 05, 2007 11:35 pm

Re: Emulación del sonido generado por la ULA

Notapor zx81 el Sab Feb 23, 2013 12:26 am

Manu escribió:radastan, el problema del zumbido ya está arreglado. Como ya he dicho en un mensaje anterior, era un tema del buffer. Había valores a cero al final, y provocaban el zumbido a 50Hz :)

Por otro lado zx81, lo que indicas acerca de los timings más o menos lo tengo en cuenta. Lo que sí es cierto es que a pesar de que la emulación del Z80 es bastante completa (con los flags ocultos, el MEMPTR, y todas las operaciones no documentadas funcionando), la del Spectrum como máquina es muy básica (no hay contienda, y el frame lo construyo al final en vez de instrucción a instrucción). De hecho, es una arquitectura que monté para poder ver los tests del Z80, que casi todos eran para Spectrum 8)


Buf, te has dejado entonces una parte importante de la emulación del Spectrum, la memoria en contienda y que, lamentablemente, tiene un impacto bastante fuerte en el core Z80. Necesitas una emulación básica de esa peculiaridad del Spectrum para que funcionen algunos juegos y para que funcione todo (incluso las demos más puñeteras) no basta con que las instrucciones tarden N estados, tienen que estar ejecutados en un cierto orden que emula a los ciclos de máquina de la CPU. Para colmo, la I/O al puerto #FE siempre es en contienda, así que de entrada tienes todos los timings mal, no esperes que suene bien del todo. Por cierto, la pantalla tampoco la puedes actualizar al final de cada frame o te vas a encontrar con muchas sorpresas.

Manu escribió:Para que te hagas una idea, la clase que emula el sonido de 1 bit es esta:

Código: Seleccionar todo
package com.emubee.sound;

public class SingleBitAudio {
   protected double microseconds;
   protected final static double MICROSECONDS_PER_CYCLE = 1000000.0 / 44100.0;
   protected boolean state;
   protected byte[] buffer;
   protected int pos;
   
   public SingleBitAudio(int samples) {
      buffer = new byte[samples * 4];   // 16 bits, stereo
      pos = 0;
   }
   
   public void reset() {
      pos = 0;
   }
   
   public void update(double time) {
      microseconds += time;
      if (microseconds >= 0) {
         buffer[pos++] = (byte) (state ? 32 : -32);
         buffer[pos++] = 0;
         buffer[pos++] = (byte) (state ? 32 : -32);
         buffer[pos++] = 0;
         microseconds -= MICROSECONDS_PER_CYCLE;
      }
   }
   
   public void setState(boolean state) {
      this.state = state;
   }
   
   public byte[] getBuffer() {
      byte[] res = new byte[pos];
      System.arraycopy(buffer, 0, res, 0, pos);
      return res;
   }
}


Y el método que "renderiza" un frame en el Spectrum es este:

Código: Seleccionar todo
public void renderFrame() {
      boolean ready = false;
      audio.reset();
      while (!ready) {
         int c = cpu.execute();
         ready = ula.update(c);
         double microseconds = c * MICROSECONDS_PER_CYCLE;
         cas.update(microseconds);
         audio.update(microseconds);
      }
      frameConverted = false;
   }


Tanto el gestor de cassete como el del audio trabajan en microsegundos y no en ciclos del Spectrum, porque los uso en otras máquinas. De hecho, la carga de cintas TZX se hace exactamente igual en el Spectrum que en el Amstrad CPC... y funciona :)

En cualquier caso, me temo que voy a tener que pelearme bastante con el sonido, ya que nunca había programado nada "multimedia" en mi vida...


El problema de ese método es que el out ejecutará un audio.setState y para cuando quieras llegar al Audio.update ya has metido un error porque la instrucción out tiene una contención al principio de la instrucción y otra al final y el cambio físico del bit se produce en medio, puedes verlo en la implementación del método Spectrum.outPort de mi emulador. Haciéndolo así te vas a equivocar en al menos 3 t-states por cada instrucción out y, aunque eso no parezca mucho, programas como Arkanoid o Fairlight cambian el bit del beeper cada 40 t-states o menos. La acumulación de errores generará mucho ruido, efectos de aliasing y otras cosas divertidas.

Tampoco es que sea muy eficiente ese bucle ejecutado instrucción a instrucción. La mayor parte del tiempo, no se ejecuta ningún out que afecte al beeper. En el caso de que no haya ningún cambio, ejecutas el Audio.update tantas veces como ejecutes el bucle while cuando en una sola llamada podrías crearlo igualmente al finalizar el frame. En cualquier caso, es imposible que te suene bien mientras no emules al menos la contención en la I/O, por eso te faltan samples al final del frame.

En fin, solo son ideas que te doy que a mi me hubieran evitado muchísimas horas de romperme el coco... :D

Edito para añadir que el error que se introduce de esa manera es incluso peor de lo que pensaba porque el error fundamental es que cuando llegas al Audio.update "parece" que todo el tiempo de ejecución de la instrucción out el bit de estado mantenía el neuvo estado y no el viejo, que es lo que sucede en realidad la mayor parte del tiempo. Para que me entiendas: si la instrucción dura 11 t-states (sin contar la contención), el cambio efectivo del bit se produce casi al final de la instrucción (digamos en el t-state 8 de 11) y cuando llegas al update actualizas el tiempo de los 11 t-states como si fueran todo uno, cuando solo 3 de los 11 pertenecen al tiempo de nuevo estado del bit del altavoz. 8 t-states de error por out son muchos (y realmente son más).

P.D.: Yo tampoco tenía ni idea de multimedia cuando me puse (y sigo sin tenerla). Pero lo que me aterraba de verdad no era el beeper, que me parecía relativamente sencillo comparado con emular a un AY-3-8912.
Debido al fallo de un mecanismo, el lanzagranadas M203 se te podía disparar cuando menos lo esperaras, lo que te habría hecho bastante impopular entre lo que quedara de tu unidad.
Revista del ejército EE.UU. PS, agosto 1993.

Emulador JSpeccy
ZXBaremulator
zx81
Freddy Hardest
 
Mensajes: 591
Registrado: Vie Dic 28, 2007 3:14 pm
Ubicación: Valencia

Re: Emulación del sonido generado por la ULA

Notapor Manu el Sab Feb 23, 2013 10:28 am

Soy consciente de que el comportamiento del emulador no es "real"... aunque se parece al modelo Inves, que si no recuerdo mal no tiene contención de memoria :)

Ahora la prioridad no es meterme con la contienda, ya que tengo otros bugs más graves... y muchas otras máquinas que quiero ver cómo funcionan.

Eso sí, muchas gracias por los consejos. Los tendré en cuenta cuando me meta en harina 8-)
Avatar de Usuario
Manu
Herbert
 
Mensajes: 89
Registrado: Mie Sep 05, 2007 11:35 pm


Volver a Emulación y preservación

¿Quién está conectado?

Usuarios navegando este Foro: No hay usuarios registrados visitando el Foro y 4 invitados

cron